mirror of
https://github.com/bitwarden/server.git
synced 2026-01-31 06:03:12 +08:00
Merge branch 'main' into auth/pm-26376/ea-delete-command
This commit is contained in:
@@ -11,3 +11,7 @@ checkmarx:
|
||||
filter: "!test"
|
||||
kics:
|
||||
filter: "!dev,!.devcontainer"
|
||||
sca:
|
||||
filter: "!dev,!.devcontainer"
|
||||
containers:
|
||||
filter: "!dev,!.devcontainer"
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
"dockerComposeFile": "../../.devcontainer/bitwarden_common/docker-compose.yml",
|
||||
"service": "bitwarden_server",
|
||||
"workspaceFolder": "/workspace",
|
||||
"initializeCommand": "mkdir -p dev/.data/keys dev/.data/mssql dev/.data/azurite dev/helpers/mssql",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "16"
|
||||
}
|
||||
"version": "22"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {}
|
||||
},
|
||||
"mounts": [
|
||||
{
|
||||
@@ -21,5 +23,27 @@
|
||||
"extensions": ["ms-dotnettools.csdevkit"]
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "bash .devcontainer/community_dev/postCreateCommand.sh"
|
||||
"postCreateCommand": "bash .devcontainer/community_dev/postCreateCommand.sh",
|
||||
"forwardPorts": [1080, 1433, 3306, 5432],
|
||||
"portsAttributes": {
|
||||
"default": {
|
||||
"onAutoForward": "ignore"
|
||||
},
|
||||
"1080": {
|
||||
"label": "Mail Catcher",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"1433": {
|
||||
"label": "SQL Server",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"3306": {
|
||||
"label": "MySQL",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"5432": {
|
||||
"label": "PostgreSQL",
|
||||
"onAutoForward": "notify"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,46 @@ export DEV_DIR=/workspace/dev
|
||||
export CONTAINER_CONFIG=/workspace/.devcontainer/community_dev
|
||||
git config --global --add safe.directory /workspace
|
||||
|
||||
if [[ -z "${CODESPACES}" ]]; then
|
||||
allow_interactive=1
|
||||
else
|
||||
echo "Doing non-interactive setup"
|
||||
allow_interactive=0
|
||||
fi
|
||||
|
||||
get_option() {
|
||||
# Helper function for reading the value of an environment variable
|
||||
# primarily but then falling back to an interactive question if allowed
|
||||
# and lastly falling back to a default value input when either other
|
||||
# option is available.
|
||||
name_of_var="$1"
|
||||
question_text="$2"
|
||||
default_value="$3"
|
||||
is_secret="$4"
|
||||
|
||||
if [[ -n "${!name_of_var}" ]]; then
|
||||
# If the env variable they gave us has a value, then use that value
|
||||
echo "${!name_of_var}"
|
||||
elif [[ "$allow_interactive" == 1 ]]; then
|
||||
# If we can be interactive, then use the text they gave us to request input
|
||||
if [[ "$is_secret" == 1 ]]; then
|
||||
read -r -s -p "$question_text" response
|
||||
echo "$response"
|
||||
else
|
||||
read -r -p "$question_text" response
|
||||
echo "$response"
|
||||
fi
|
||||
else
|
||||
# If no environment variable and not interactive, then just give back default value
|
||||
echo "$default_value"
|
||||
fi
|
||||
}
|
||||
|
||||
get_installation_id_and_key() {
|
||||
pushd ./dev >/dev/null || exit
|
||||
echo "Please enter your installation id and key from https://bitwarden.com/host:"
|
||||
read -r -p "Installation id: " INSTALLATION_ID
|
||||
read -r -p "Installation key: " INSTALLATION_KEY
|
||||
INSTALLATION_ID="$(get_option "INSTALLATION_ID" "Installation id: " "00000000-0000-0000-0000-000000000001")"
|
||||
INSTALLATION_KEY="$(get_option "INSTALLATION_KEY" "Installation key: " "" 1)"
|
||||
jq ".globalSettings.installation.id = \"$INSTALLATION_ID\" |
|
||||
.globalSettings.installation.key = \"$INSTALLATION_KEY\"" \
|
||||
secrets.json.example >secrets.json # create/overwrite secrets.json
|
||||
@@ -30,11 +65,10 @@ configure_other_vars() {
|
||||
}
|
||||
|
||||
one_time_setup() {
|
||||
read -r -p \
|
||||
"Would you like to configure your secrets and certificates for the first time?
|
||||
do_secrets_json_setup="$(get_option "SETUP_SECRETS_JSON" "Would you like to configure your secrets and certificates for the first time?
|
||||
WARNING: This will overwrite any existing secrets.json and certificate files.
|
||||
Proceed? [y/N] " response
|
||||
if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
Proceed? [y/N] " "n")"
|
||||
if [[ "$do_secrets_json_setup" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo "Running one-time setup script..."
|
||||
sleep 1
|
||||
get_installation_id_and_key
|
||||
@@ -50,11 +84,4 @@ Proceed? [y/N] " response
|
||||
fi
|
||||
}
|
||||
|
||||
# main
|
||||
if [[ -z "${CODESPACES}" ]]; then
|
||||
one_time_setup
|
||||
else
|
||||
# Ignore interactive elements when running in codespaces since they are not supported there
|
||||
# TODO Write codespaces specific instructions and link here
|
||||
echo "Running in codespaces, follow instructions here: https://contributing.bitwarden.com/getting-started/server/guide/ to continue the setup"
|
||||
fi
|
||||
one_time_setup
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
],
|
||||
"service": "bitwarden_server",
|
||||
"workspaceFolder": "/workspace",
|
||||
"initializeCommand": "mkdir -p dev/.data/keys dev/.data/mssql dev/.data/azurite dev/helpers/mssql",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "16"
|
||||
}
|
||||
"version": "22"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {}
|
||||
},
|
||||
"mounts": [
|
||||
{
|
||||
@@ -24,9 +26,18 @@
|
||||
"extensions": ["ms-dotnettools.csdevkit"]
|
||||
}
|
||||
},
|
||||
"onCreateCommand": "bash .devcontainer/internal_dev/onCreateCommand.sh",
|
||||
"postCreateCommand": "bash .devcontainer/internal_dev/postCreateCommand.sh",
|
||||
"forwardPorts": [1080, 1433, 3306, 5432, 10000, 10001, 10002],
|
||||
"forwardPorts": [
|
||||
1080, 1433, 3306, 5432, 10000, 10001, 10002,
|
||||
4000, 4001, 33656, 33657, 44519, 44559,
|
||||
46273, 46274, 50024, 51822, 51823,
|
||||
54103, 61840, 61841, 62911, 62912
|
||||
],
|
||||
"portsAttributes": {
|
||||
"default": {
|
||||
"onAutoForward": "ignore"
|
||||
},
|
||||
"1080": {
|
||||
"label": "Mail Catcher",
|
||||
"onAutoForward": "notify"
|
||||
@@ -48,12 +59,76 @@
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"10001": {
|
||||
"label": "Azurite Storage Queue ",
|
||||
"label": "Azurite Storage Queue",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"10002": {
|
||||
"label": "Azurite Storage Table",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"4000": {
|
||||
"label": "Api (Cloud)",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"4001": {
|
||||
"label": "Api (SelfHost)",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"33656": {
|
||||
"label": "Identity (Cloud)",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"33657": {
|
||||
"label": "Identity (SelfHost)",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"44519": {
|
||||
"label": "Billing",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"44559": {
|
||||
"label": "Scim",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"46273": {
|
||||
"label": "Events (Cloud)",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"46274": {
|
||||
"label": "Events (SelfHost)",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"50024": {
|
||||
"label": "Icons",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"51822": {
|
||||
"label": "Sso (Cloud)",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"51823": {
|
||||
"label": "Sso (SelfHost)",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"54103": {
|
||||
"label": "EventsProcessor",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"61840": {
|
||||
"label": "Notifications (Cloud)",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"61841": {
|
||||
"label": "Notifications (SelfHost)",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"62911": {
|
||||
"label": "Admin (Cloud)",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"62912": {
|
||||
"label": "Admin (SelfHost)",
|
||||
"onAutoForward": "notify"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
.devcontainer/internal_dev/onCreateCommand.sh
Normal file
12
.devcontainer/internal_dev/onCreateCommand.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
export REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
|
||||
file="$REPO_ROOT/dev/custom-root-ca.crt"
|
||||
|
||||
if [ -e "$file" ]; then
|
||||
echo "Adding custom root CA"
|
||||
sudo cp "$file" /usr/local/share/ca-certificates/
|
||||
sudo update-ca-certificates
|
||||
else
|
||||
echo "No custom root CA found, skipping..."
|
||||
fi
|
||||
@@ -108,7 +108,7 @@ Press <Enter> to continue."
|
||||
fi
|
||||
|
||||
run_mssql_migrations="$(get_option "RUN_MSSQL_MIGRATIONS" "Would you like us to run MSSQL Migrations for you? [y/N] " "n")"
|
||||
if [[ "$do_azurite_setup" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
if [[ "$run_mssql_migrations" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo "Running migrations..."
|
||||
sleep 5 # wait for DB container to start
|
||||
dotnet run --project "$REPO_ROOT/util/MsSqlMigratorUtility" "$SQL_CONNECTION_STRING"
|
||||
|
||||
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
@@ -11,6 +11,9 @@
|
||||
**/docker-compose.yml @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre
|
||||
|
||||
# Scanning tools
|
||||
.checkmarx/ @bitwarden/team-appsec
|
||||
|
||||
## BRE team owns these workflows ##
|
||||
.github/workflows/publish.yml @bitwarden/dept-bre
|
||||
|
||||
@@ -94,9 +97,7 @@ src/Admin/Views/Tools @bitwarden/team-billing-dev
|
||||
.github/workflows/test-database.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/test.yml @bitwarden/team-platform-dev
|
||||
**/*Platform* @bitwarden/team-platform-dev
|
||||
**/.dockerignore @bitwarden/team-platform-dev
|
||||
**/Dockerfile @bitwarden/team-platform-dev
|
||||
**/entrypoint.sh @bitwarden/team-platform-dev
|
||||
|
||||
# The PushType enum is expected to be editted by anyone without need for Platform review
|
||||
src/Core/Platform/Push/PushType.cs
|
||||
|
||||
|
||||
14
.github/renovate.json5
vendored
14
.github/renovate.json5
vendored
@@ -21,12 +21,6 @@
|
||||
commitMessagePrefix: "[deps] AC:",
|
||||
reviewers: ["team:team-admin-console-dev"],
|
||||
},
|
||||
{
|
||||
matchFileNames: ["src/Admin/package.json", "src/Sso/package.json"],
|
||||
description: "Admin & SSO npm packages",
|
||||
commitMessagePrefix: "[deps] Auth:",
|
||||
reviewers: ["team:team-auth-dev"],
|
||||
},
|
||||
{
|
||||
matchPackageNames: [
|
||||
"DuoUniversal",
|
||||
@@ -182,6 +176,14 @@
|
||||
matchUpdateTypes: ["minor"],
|
||||
addLabels: ["hold"],
|
||||
},
|
||||
{
|
||||
groupName: "Admin and SSO npm dependencies",
|
||||
matchFileNames: ["src/Admin/package.json", "src/Sso/package.json"],
|
||||
matchUpdateTypes: ["minor", "patch"],
|
||||
description: "Admin & SSO npm packages",
|
||||
commitMessagePrefix: "[deps] Auth:",
|
||||
reviewers: ["team:team-auth-dev"],
|
||||
},
|
||||
{
|
||||
matchPackageNames: ["/^Microsoft\\.EntityFrameworkCore\\./", "/^dotnet-ef/"],
|
||||
groupName: "EntityFrameworkCore",
|
||||
|
||||
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
|
||||
- name: Verify format
|
||||
run: dotnet format --verify-no-changes
|
||||
@@ -119,10 +119,10 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
|
||||
- name: Install Cosign
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
uses: sigstore/cosign-installer@7e8b541eb2e61bf99390e1afd4be13a184e9ebc5 # v3.10.1
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
|
||||
- name: Sign image with Cosign
|
||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||
@@ -263,14 +263,14 @@ jobs:
|
||||
|
||||
- name: Scan Docker image
|
||||
id: container-scan
|
||||
uses: anchore/scan-action@3c9a191a0fbab285ca6b8530b5de5a642cba332f # v7.2.2
|
||||
uses: anchore/scan-action@62b74fb7bb810d2c45b1865f47a77655621862a5 # v7.2.3
|
||||
with:
|
||||
image: ${{ steps.image-tags.outputs.primary_tag }}
|
||||
fail-build: false
|
||||
output-format: sarif
|
||||
|
||||
- name: Upload Grype results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
with:
|
||||
sarif_file: ${{ steps.container-scan.outputs.sarif }}
|
||||
sha: ${{ contains(github.event_name, 'pull_request') && github.event.pull_request.head.sha || github.sha }}
|
||||
@@ -294,7 +294,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
@@ -420,7 +420,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
|
||||
6
.github/workflows/test-database.yml
vendored
6
.github/workflows/test-database.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
|
||||
- name: Restore tools
|
||||
run: dotnet tool restore
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
run: 'docker logs "$(docker ps --quiet --filter "name=mssql")"'
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0
|
||||
uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||
with:
|
||||
name: Test Results
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 # v5.0.1
|
||||
uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0
|
||||
|
||||
- name: Install rust
|
||||
uses: dtolnay/rust-toolchain@f7ccc83f9ed1e5b9c81d8a67d7ad1a747e22a561 # stable
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
run: dotnet test ./bitwarden_license/test --configuration Debug --logger "trx;LogFileName=bw-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
||||
|
||||
- name: Report test results
|
||||
uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0
|
||||
uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !cancelled() }}
|
||||
with:
|
||||
name: Test Results
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2026.1.0</Version>
|
||||
<Version>2026.1.1</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
@@ -45,7 +45,7 @@ public class AccountController : Controller
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly ISsoUserRepository _ssoUserRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IPolicyQuery _policyQuery;
|
||||
private readonly IUserService _userService;
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly UserManager<User> _userManager;
|
||||
@@ -67,7 +67,7 @@ public class AccountController : Controller
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
ISsoUserRepository ssoUserRepository,
|
||||
IUserRepository userRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
IPolicyQuery policyQuery,
|
||||
IUserService userService,
|
||||
II18nService i18nService,
|
||||
UserManager<User> userManager,
|
||||
@@ -88,7 +88,7 @@ public class AccountController : Controller
|
||||
_userRepository = userRepository;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_ssoUserRepository = ssoUserRepository;
|
||||
_policyRepository = policyRepository;
|
||||
_policyQuery = policyQuery;
|
||||
_userService = userService;
|
||||
_i18nService = i18nService;
|
||||
_userManager = userManager;
|
||||
@@ -687,9 +687,8 @@ public class AccountController : Controller
|
||||
await _registerUserCommand.RegisterSSOAutoProvisionedUserAsync(newUser, organization);
|
||||
|
||||
// If the organization has 2fa policy enabled, make sure to default jit user 2fa to email
|
||||
var twoFactorPolicy =
|
||||
await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.TwoFactorAuthentication);
|
||||
if (twoFactorPolicy != null && twoFactorPolicy.Enabled)
|
||||
var twoFactorPolicy = await _policyQuery.RunAsync(organization.Id, PolicyType.TwoFactorAuthentication);
|
||||
if (twoFactorPolicy.Enabled)
|
||||
{
|
||||
newUser.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
|
||||
@@ -8,7 +8,6 @@ using Bit.Core.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Bit.Sso.Utilities;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Microsoft.IdentityModel.Logging;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Sso;
|
||||
@@ -91,20 +90,15 @@ public class Startup
|
||||
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IWebHostEnvironment env,
|
||||
IWebHostEnvironment environment,
|
||||
IHostApplicationLifetime appLifetime,
|
||||
GlobalSettings globalSettings,
|
||||
ILogger<Startup> logger)
|
||||
{
|
||||
if (env.IsDevelopment() || globalSettings.SelfHosted)
|
||||
{
|
||||
IdentityModelEventSource.ShowPII = true;
|
||||
}
|
||||
|
||||
// Add general security headers
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
|
||||
if (!env.IsDevelopment())
|
||||
if (!environment.IsDevelopment())
|
||||
{
|
||||
var uri = new Uri(globalSettings.BaseServiceUri.Sso);
|
||||
app.Use(async (ctx, next) =>
|
||||
@@ -120,7 +114,7 @@ public class Startup
|
||||
app.UseForwardedHeaders(globalSettings);
|
||||
}
|
||||
|
||||
if (env.IsDevelopment())
|
||||
if (environment.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseCookiePolicy();
|
||||
|
||||
@@ -34,4 +34,5 @@ RABBITMQ_DEFAULT_PASS=SET_A_PASSWORD_HERE_123
|
||||
# SETUP_AZURITE=yes
|
||||
# RUN_MSSQL_MIGRATIONS=yes
|
||||
# DEV_CERT_PASSWORD=dev_cert_password_here
|
||||
# DEV_CERT_CONTENTS=base64_encoded_dev_pfx_here (alternative to placing dev.pfx file manually)
|
||||
# INSTALL_STRIPE_CLI=no
|
||||
|
||||
1
dev/.gitignore
vendored
1
dev/.gitignore
vendored
@@ -18,3 +18,4 @@ signingkey.jwk
|
||||
|
||||
# Reverse Proxy Conifg
|
||||
reverse-proxy.conf
|
||||
*.crt
|
||||
|
||||
@@ -57,7 +57,7 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IPolicyQuery _policyQuery;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;
|
||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||
@@ -90,7 +90,7 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
ICollectionRepository collectionRepository,
|
||||
IGroupRepository groupRepository,
|
||||
IUserService userService,
|
||||
IPolicyRepository policyRepository,
|
||||
IPolicyQuery policyQuery,
|
||||
ICurrentContext currentContext,
|
||||
ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery,
|
||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||
@@ -123,7 +123,7 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
_collectionRepository = collectionRepository;
|
||||
_groupRepository = groupRepository;
|
||||
_userService = userService;
|
||||
_policyRepository = policyRepository;
|
||||
_policyQuery = policyQuery;
|
||||
_currentContext = currentContext;
|
||||
_countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;
|
||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||
@@ -287,14 +287,7 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
|
||||
IEnumerable<Tuple<Core.Entities.OrganizationUser, string>> result;
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.IncreaseBulkReinviteLimitForCloud))
|
||||
{
|
||||
result = await _bulkResendOrganizationInvitesCommand.BulkResendInvitesAsync(orgId, userId.Value, model.Ids);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = await _organizationService.ResendInvitesAsync(orgId, userId.Value, model.Ids);
|
||||
}
|
||||
result = await _bulkResendOrganizationInvitesCommand.BulkResendInvitesAsync(orgId, userId.Value, model.Ids);
|
||||
|
||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(
|
||||
result.Select(t => new OrganizationUserBulkResponseModel(t.Item1.Id, t.Item2)));
|
||||
@@ -357,10 +350,9 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
return false;
|
||||
}
|
||||
|
||||
var masterPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
||||
var useMasterPasswordPolicy = masterPasswordPolicy != null &&
|
||||
masterPasswordPolicy.Enabled &&
|
||||
masterPasswordPolicy.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled;
|
||||
var masterPasswordPolicy = await _policyQuery.RunAsync(orgId, PolicyType.ResetPassword);
|
||||
var useMasterPasswordPolicy = masterPasswordPolicy.Enabled &&
|
||||
masterPasswordPolicy.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled;
|
||||
|
||||
return useMasterPasswordPolicy;
|
||||
}
|
||||
@@ -697,7 +689,16 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task RestoreAsync(Guid orgId, Guid id)
|
||||
{
|
||||
await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _restoreOrganizationUserCommand.RestoreUserAsync(orgUser, userId));
|
||||
await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _restoreOrganizationUserCommand.RestoreUserAsync(orgUser, userId, null));
|
||||
}
|
||||
|
||||
|
||||
[HttpPut("{id}/restore/vnext")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
[RequireFeature(FeatureFlagKeys.DefaultUserCollectionRestore)]
|
||||
public async Task RestoreAsync_vNext(Guid orgId, Guid id, [FromBody] OrganizationUserRestoreRequest request)
|
||||
{
|
||||
await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _restoreOrganizationUserCommand.RestoreUserAsync(orgUser, userId, request.DefaultUserCollectionName));
|
||||
}
|
||||
|
||||
[HttpPatch("{id}/restore")]
|
||||
@@ -712,7 +713,9 @@ public class OrganizationUsersController : BaseAdminConsoleController
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRestoreAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
return await RestoreOrRevokeUsersAsync(orgId, model, (orgId, orgUserIds, restoringUserId) => _restoreOrganizationUserCommand.RestoreUsersAsync(orgId, orgUserIds, restoringUserId, _userService));
|
||||
return await RestoreOrRevokeUsersAsync(orgId, model,
|
||||
(orgId, orgUserIds, restoringUserId) => _restoreOrganizationUserCommand.RestoreUsersAsync(orgId, orgUserIds,
|
||||
restoringUserId, _userService, model.DefaultUserCollectionName));
|
||||
}
|
||||
|
||||
[HttpPatch("restore")]
|
||||
|
||||
@@ -48,7 +48,7 @@ public class OrganizationsController : Controller
|
||||
{
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IPolicyQuery _policyQuery;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
@@ -74,7 +74,7 @@ public class OrganizationsController : Controller
|
||||
public OrganizationsController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
IPolicyQuery policyQuery,
|
||||
IOrganizationService organizationService,
|
||||
IUserService userService,
|
||||
ICurrentContext currentContext,
|
||||
@@ -99,7 +99,7 @@ public class OrganizationsController : Controller
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_policyRepository = policyRepository;
|
||||
_policyQuery = policyQuery;
|
||||
_organizationService = organizationService;
|
||||
_userService = userService;
|
||||
_currentContext = currentContext;
|
||||
@@ -183,15 +183,14 @@ public class OrganizationsController : Controller
|
||||
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, resetPasswordPolicyRequirement.AutoEnrollEnabled(organization.Id));
|
||||
}
|
||||
|
||||
var resetPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
|
||||
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled || resetPasswordPolicy.Data == null)
|
||||
var resetPasswordPolicy = await _policyQuery.RunAsync(organization.Id, PolicyType.ResetPassword);
|
||||
if (!resetPasswordPolicy.Enabled || resetPasswordPolicy.Data == null)
|
||||
{
|
||||
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, false);
|
||||
}
|
||||
|
||||
var data = JsonSerializer.Deserialize<ResetPasswordDataModel>(resetPasswordPolicy.Data, JsonHelpers.IgnoreCase);
|
||||
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false);
|
||||
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
|
||||
@@ -7,7 +7,6 @@ using Bit.Api.AdminConsole.Models.Request;
|
||||
using Bit.Api.AdminConsole.Models.Response.Helpers;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
@@ -43,6 +42,7 @@ public class PoliciesController : Controller
|
||||
private readonly IUserService _userService;
|
||||
private readonly ISavePolicyCommand _savePolicyCommand;
|
||||
private readonly IVNextSavePolicyCommand _vNextSavePolicyCommand;
|
||||
private readonly IPolicyQuery _policyQuery;
|
||||
|
||||
public PoliciesController(IPolicyRepository policyRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@@ -54,7 +54,8 @@ public class PoliciesController : Controller
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IVNextSavePolicyCommand vNextSavePolicyCommand)
|
||||
IVNextSavePolicyCommand vNextSavePolicyCommand,
|
||||
IPolicyQuery policyQuery)
|
||||
{
|
||||
_policyRepository = policyRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@@ -68,27 +69,24 @@ public class PoliciesController : Controller
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
_savePolicyCommand = savePolicyCommand;
|
||||
_vNextSavePolicyCommand = vNextSavePolicyCommand;
|
||||
_policyQuery = policyQuery;
|
||||
}
|
||||
|
||||
[HttpGet("{type}")]
|
||||
public async Task<PolicyDetailResponseModel> Get(Guid orgId, int type)
|
||||
public async Task<PolicyStatusResponseModel> Get(Guid orgId, PolicyType type)
|
||||
{
|
||||
if (!await _currentContext.ManagePolicies(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type);
|
||||
if (policy == null)
|
||||
{
|
||||
return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type });
|
||||
}
|
||||
|
||||
var policy = await _policyQuery.RunAsync(orgId, type);
|
||||
if (policy.Type is PolicyType.SingleOrg)
|
||||
{
|
||||
return await policy.GetSingleOrgPolicyDetailResponseAsync(_organizationHasVerifiedDomainsQuery);
|
||||
return await policy.GetSingleOrgPolicyStatusResponseAsync(_organizationHasVerifiedDomainsQuery);
|
||||
}
|
||||
|
||||
return new PolicyDetailResponseModel(policy);
|
||||
return new PolicyStatusResponseModel(policy);
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request;
|
||||
|
||||
public class OrganizationDomainRequestModel
|
||||
{
|
||||
[Required]
|
||||
[DomainNameValidator]
|
||||
public string DomainName { get; set; }
|
||||
}
|
||||
|
||||
@@ -116,12 +116,17 @@ public class OrganizationUserResetPasswordEnrollmentRequestModel
|
||||
public string ResetPasswordKey { get; set; }
|
||||
public string MasterPasswordHash { get; set; }
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
public class OrganizationUserBulkRequestModel
|
||||
{
|
||||
[Required, MinLength(1)]
|
||||
public IEnumerable<Guid> Ids { get; set; }
|
||||
public IEnumerable<Guid> Ids { get; set; } = new List<Guid>();
|
||||
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(1000)]
|
||||
public string? DefaultUserCollectionName { get; set; }
|
||||
}
|
||||
#nullable disable
|
||||
|
||||
public class ResetPasswordWithOrgIdRequestModel : OrganizationUserResetPasswordEnrollmentRequestModel
|
||||
{
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
|
||||
public class OrganizationUserRestoreRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// This is the encrypted default collection name to be used for restored users if required
|
||||
/// </summary>
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(1000)]
|
||||
public string? DefaultUserCollectionName { get; set; }
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Response.Helpers;
|
||||
|
||||
public static class PolicyDetailResponses
|
||||
public static class PolicyStatusResponses
|
||||
{
|
||||
public static async Task<PolicyDetailResponseModel> GetSingleOrgPolicyDetailResponseAsync(this Policy policy, IOrganizationHasVerifiedDomainsQuery hasVerifiedDomainsQuery)
|
||||
public static async Task<PolicyStatusResponseModel> GetSingleOrgPolicyStatusResponseAsync(
|
||||
this PolicyStatus policy, IOrganizationHasVerifiedDomainsQuery hasVerifiedDomainsQuery)
|
||||
{
|
||||
if (policy.Type is not PolicyType.SingleOrg)
|
||||
{
|
||||
throw new ArgumentException($"'{nameof(policy)}' must be of type '{nameof(PolicyType.SingleOrg)}'.", nameof(policy));
|
||||
}
|
||||
return new PolicyDetailResponseModel(policy, await CanToggleState());
|
||||
|
||||
return new PolicyStatusResponseModel(policy, await CanToggleState());
|
||||
|
||||
async Task<bool> CanToggleState()
|
||||
{
|
||||
@@ -25,5 +27,4 @@ public static class PolicyDetailResponses
|
||||
return !policy.Enabled;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
|
||||
public class PolicyDetailResponseModel : PolicyResponseModel
|
||||
{
|
||||
public PolicyDetailResponseModel(Policy policy, string obj = "policy") : base(policy, obj)
|
||||
{
|
||||
}
|
||||
|
||||
public PolicyDetailResponseModel(Policy policy, bool canToggleState) : base(policy)
|
||||
{
|
||||
CanToggleState = canToggleState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the Policy can be enabled/disabled
|
||||
/// </summary>
|
||||
public bool CanToggleState { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
|
||||
public class PolicyStatusResponseModel : ResponseModel
|
||||
{
|
||||
public PolicyStatusResponseModel(PolicyStatus policy, bool canToggleState = true) : base("policy")
|
||||
{
|
||||
OrganizationId = policy.OrganizationId;
|
||||
Type = policy.Type;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(policy.Data))
|
||||
{
|
||||
Data = JsonSerializer.Deserialize<Dictionary<string, object>>(policy.Data) ?? new();
|
||||
}
|
||||
|
||||
Enabled = policy.Enabled;
|
||||
CanToggleState = canToggleState;
|
||||
}
|
||||
|
||||
public Guid OrganizationId { get; init; }
|
||||
public PolicyType Type { get; init; }
|
||||
public Dictionary<string, object> Data { get; init; } = new();
|
||||
public bool Enabled { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the Policy can be enabled/disabled
|
||||
/// </summary>
|
||||
public bool CanToggleState { get; init; }
|
||||
}
|
||||
@@ -2,12 +2,16 @@
|
||||
using Bit.Api.AdminConsole.Public.Models.Request;
|
||||
using Bit.Api.AdminConsole.Public.Models.Response;
|
||||
using Bit.Api.Models.Public.Response;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -30,6 +34,8 @@ public class MembersController : Controller
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
|
||||
private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommandV2;
|
||||
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||
|
||||
public MembersController(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@@ -42,7 +48,9 @@ public class MembersController : Controller
|
||||
IOrganizationRepository organizationRepository,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand)
|
||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand,
|
||||
IRevokeOrganizationUserCommand revokeOrganizationUserCommandV2,
|
||||
IRestoreOrganizationUserCommand restoreOrganizationUserCommand)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_groupRepository = groupRepository;
|
||||
@@ -55,6 +63,8 @@ public class MembersController : Controller
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
|
||||
_revokeOrganizationUserCommandV2 = revokeOrganizationUserCommandV2;
|
||||
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -258,4 +268,59 @@ public class MembersController : Controller
|
||||
await _resendOrganizationInviteCommand.ResendInviteAsync(_currentContext.OrganizationId!.Value, null, id);
|
||||
return new OkResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Revoke a member's access to an organization.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the member to be revoked.</param>
|
||||
[HttpPost("{id}/revoke")]
|
||||
[ProducesResponseType((int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> Revoke(Guid id)
|
||||
{
|
||||
var organizationUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||
if (organizationUser == null || organizationUser.OrganizationId != _currentContext.OrganizationId)
|
||||
{
|
||||
return new NotFoundResult();
|
||||
}
|
||||
|
||||
var request = new RevokeOrganizationUsersRequest(
|
||||
_currentContext.OrganizationId!.Value,
|
||||
[id],
|
||||
new SystemUser(EventSystemUser.PublicApi)
|
||||
);
|
||||
|
||||
var results = await _revokeOrganizationUserCommandV2.RevokeUsersAsync(request);
|
||||
var result = results.Single();
|
||||
|
||||
return result.Result.Match<IActionResult>(
|
||||
error => new BadRequestObjectResult(new ErrorResponseModel(error.Message)),
|
||||
_ => new OkResult()
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore a member.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Restores a previously revoked member of the organization.
|
||||
/// </remarks>
|
||||
/// <param name="id">The identifier of the member to be restored.</param>
|
||||
[HttpPost("{id}/restore")]
|
||||
[ProducesResponseType((int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> Restore(Guid id)
|
||||
{
|
||||
var organizationUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||
if (organizationUser == null || organizationUser.OrganizationId != _currentContext.OrganizationId)
|
||||
{
|
||||
return new NotFoundResult();
|
||||
}
|
||||
|
||||
await _restoreOrganizationUserCommand.RestoreUserAsync(organizationUser, EventSystemUser.PublicApi);
|
||||
|
||||
return new OkResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ using Bit.Api.Models.Response;
|
||||
using Bit.Api.Models.Response.Organizations;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
@@ -38,7 +38,7 @@ public class OrganizationSponsorshipsController : Controller
|
||||
private readonly ICloudSyncSponsorshipsCommand _syncSponsorshipsCommand;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IPolicyQuery _policyQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public OrganizationSponsorshipsController(
|
||||
@@ -55,7 +55,7 @@ public class OrganizationSponsorshipsController : Controller
|
||||
ICloudSyncSponsorshipsCommand syncSponsorshipsCommand,
|
||||
IUserService userService,
|
||||
ICurrentContext currentContext,
|
||||
IPolicyRepository policyRepository,
|
||||
IPolicyQuery policyQuery,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_organizationSponsorshipRepository = organizationSponsorshipRepository;
|
||||
@@ -71,7 +71,7 @@ public class OrganizationSponsorshipsController : Controller
|
||||
_syncSponsorshipsCommand = syncSponsorshipsCommand;
|
||||
_userService = userService;
|
||||
_currentContext = currentContext;
|
||||
_policyRepository = policyRepository;
|
||||
_policyQuery = policyQuery;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
@@ -81,10 +81,10 @@ public class OrganizationSponsorshipsController : Controller
|
||||
public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipCreateRequestModel model)
|
||||
{
|
||||
var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId);
|
||||
var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsoringOrgId,
|
||||
var freeFamiliesSponsorshipPolicy = await _policyQuery.RunAsync(sponsoringOrgId,
|
||||
PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||
|
||||
if (freeFamiliesSponsorshipPolicy?.Enabled == true)
|
||||
if (freeFamiliesSponsorshipPolicy.Enabled)
|
||||
{
|
||||
throw new BadRequestException("Free Bitwarden Families sponsorship has been disabled by your organization administrator.");
|
||||
}
|
||||
@@ -108,10 +108,10 @@ public class OrganizationSponsorshipsController : Controller
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task ResendSponsorshipOffer(Guid sponsoringOrgId, [FromQuery] string sponsoredFriendlyName)
|
||||
{
|
||||
var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsoringOrgId,
|
||||
var freeFamiliesSponsorshipPolicy = await _policyQuery.RunAsync(sponsoringOrgId,
|
||||
PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||
|
||||
if (freeFamiliesSponsorshipPolicy?.Enabled == true)
|
||||
if (freeFamiliesSponsorshipPolicy.Enabled)
|
||||
{
|
||||
throw new BadRequestException("Free Bitwarden Families sponsorship has been disabled by your organization administrator.");
|
||||
}
|
||||
@@ -138,9 +138,9 @@ public class OrganizationSponsorshipsController : Controller
|
||||
var (isValid, sponsorship) = await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email);
|
||||
if (isValid && sponsorship.SponsoringOrganizationId.HasValue)
|
||||
{
|
||||
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsorship.SponsoringOrganizationId.Value,
|
||||
var policy = await _policyQuery.RunAsync(sponsorship.SponsoringOrganizationId.Value,
|
||||
PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||
isFreeFamilyPolicyEnabled = policy?.Enabled ?? false;
|
||||
isFreeFamilyPolicyEnabled = policy.Enabled;
|
||||
}
|
||||
|
||||
var response = PreValidateSponsorshipResponseModel.From(isValid, isFreeFamilyPolicyEnabled);
|
||||
@@ -165,10 +165,10 @@ public class OrganizationSponsorshipsController : Controller
|
||||
throw new BadRequestException("Can only redeem sponsorship for an organization you own.");
|
||||
}
|
||||
|
||||
var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(
|
||||
var freeFamiliesSponsorshipPolicy = await _policyQuery.RunAsync(
|
||||
model.SponsoredOrganizationId, PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||
|
||||
if (freeFamiliesSponsorshipPolicy?.Enabled == true)
|
||||
if (freeFamiliesSponsorshipPolicy.Enabled)
|
||||
{
|
||||
throw new BadRequestException("Free Bitwarden Families sponsorship has been disabled by your organization administrator.");
|
||||
}
|
||||
|
||||
@@ -34,8 +34,7 @@ public class OrganizationUserRotationValidator : IRotationValidator<IEnumerable<
|
||||
}
|
||||
|
||||
// Exclude any account recovery that do not have a key.
|
||||
existing = existing.Where(o => o.ResetPasswordKey != null).ToList();
|
||||
|
||||
existing = existing.Where(o => !string.IsNullOrEmpty(o.ResetPasswordKey)).ToList();
|
||||
|
||||
foreach (var ou in existing)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace Bit.Api.Models.Public.Response;
|
||||
@@ -46,13 +44,14 @@ public class ErrorResponseModel : IResponseModel
|
||||
{ }
|
||||
|
||||
public ErrorResponseModel(string errorKey, string errorValue)
|
||||
: this(errorKey, new string[] { errorValue })
|
||||
: this(errorKey, [errorValue])
|
||||
{ }
|
||||
|
||||
public ErrorResponseModel(string errorKey, IEnumerable<string> errorValues)
|
||||
: this(new Dictionary<string, IEnumerable<string>> { { errorKey, errorValues } })
|
||||
{ }
|
||||
|
||||
[JsonConstructor]
|
||||
public ErrorResponseModel(string message, Dictionary<string, IEnumerable<string>> errors)
|
||||
{
|
||||
Message = message;
|
||||
@@ -70,10 +69,10 @@ public class ErrorResponseModel : IResponseModel
|
||||
/// </summary>
|
||||
/// <example>The request model is invalid.</example>
|
||||
[Required]
|
||||
public string Message { get; set; }
|
||||
public string Message { get; init; }
|
||||
/// <summary>
|
||||
/// If multiple errors occurred, they are listed in dictionary. Errors related to a specific
|
||||
/// request parameter will include a dictionary key describing that parameter.
|
||||
/// </summary>
|
||||
public Dictionary<string, IEnumerable<string>> Errors { get; set; }
|
||||
public Dictionary<string, IEnumerable<string>>? Errors { get; }
|
||||
}
|
||||
|
||||
@@ -67,8 +67,9 @@ public class CollectionsController : Controller
|
||||
{
|
||||
var collections = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(_currentContext.OrganizationId.Value);
|
||||
|
||||
var collectionResponses = collections.Select(c =>
|
||||
new CollectionResponseModel(c.Item1, c.Item2.Groups));
|
||||
var collectionResponses = collections
|
||||
.Where(c => c.Item1.Type != CollectionType.DefaultUserCollection)
|
||||
.Select(c => new CollectionResponseModel(c.Item1, c.Item2.Groups));
|
||||
|
||||
var response = new ListResponseModel<CollectionResponseModel>(collectionResponses);
|
||||
return new JsonResult(response);
|
||||
|
||||
@@ -14,7 +14,6 @@ using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.SharedWeb.Health;
|
||||
using Microsoft.IdentityModel.Logging;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||
@@ -238,8 +237,6 @@ public class Startup
|
||||
GlobalSettings globalSettings,
|
||||
ILogger<Startup> logger)
|
||||
{
|
||||
IdentityModelEventSource.ShowPII = true;
|
||||
|
||||
// Add general security headers
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
|
||||
|
||||
@@ -239,12 +239,6 @@ public class SendsController : Controller
|
||||
{
|
||||
throw new BadRequestException("Could not locate send");
|
||||
}
|
||||
if (send.MaxAccessCount.GetValueOrDefault(int.MaxValue) <= send.AccessCount ||
|
||||
send.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < DateTime.UtcNow || send.Disabled ||
|
||||
send.DeletionDate < DateTime.UtcNow)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var sendResponse = new SendAccessResponseModel(send);
|
||||
if (send.UserId.HasValue && !send.HideEmail.GetValueOrDefault())
|
||||
@@ -272,12 +266,6 @@ public class SendsController : Controller
|
||||
{
|
||||
throw new BadRequestException("Could not locate send");
|
||||
}
|
||||
if (send.MaxAccessCount.GetValueOrDefault(int.MaxValue) <= send.AccessCount ||
|
||||
send.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < DateTime.UtcNow || send.Disabled ||
|
||||
send.DeletionDate < DateTime.UtcNow)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var url = await _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId);
|
||||
|
||||
|
||||
@@ -102,9 +102,17 @@ public class SendRequestModel
|
||||
/// Comma-separated list of emails that may access the send using OTP
|
||||
/// authentication. Mutually exclusive with <see cref="Password"/>.
|
||||
/// </summary>
|
||||
[StringLength(4000)]
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(4000)]
|
||||
public string Emails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Comma-separated list of email **hashes** that may access the send using OTP
|
||||
/// authentication. Mutually exclusive with <see cref="Password"/>.
|
||||
/// </summary>
|
||||
[StringLength(4000)]
|
||||
public string EmailHashes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When <see langword="true"/>, send access is disabled.
|
||||
/// Defaults to <see langword="false"/>.
|
||||
@@ -253,6 +261,7 @@ public class SendRequestModel
|
||||
// normalize encoding
|
||||
var emails = Emails.Split(',', RemoveEmptyEntries | TrimEntries);
|
||||
existingSend.Emails = string.Join(",", emails);
|
||||
existingSend.EmailHashes = EmailHashes;
|
||||
existingSend.Password = null;
|
||||
existingSend.AuthType = Core.Tools.Enums.AuthType.Email;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@@ -44,6 +45,7 @@ public class SyncController : Controller
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository;
|
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||
|
||||
public SyncController(
|
||||
@@ -61,6 +63,7 @@ public class SyncController : Controller
|
||||
IFeatureService featureService,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IWebAuthnCredentialRepository webAuthnCredentialRepository,
|
||||
IUserAccountKeysQuery userAccountKeysQuery)
|
||||
{
|
||||
_userService = userService;
|
||||
@@ -77,6 +80,7 @@ public class SyncController : Controller
|
||||
_featureService = featureService;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_webAuthnCredentialRepository = webAuthnCredentialRepository;
|
||||
_userAccountKeysQuery = userAccountKeysQuery;
|
||||
}
|
||||
|
||||
@@ -120,6 +124,9 @@ public class SyncController : Controller
|
||||
var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);
|
||||
|
||||
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
var webAuthnCredentials = _featureService.IsEnabled(FeatureFlagKeys.PM2035PasskeyUnlock)
|
||||
? await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id)
|
||||
: [];
|
||||
|
||||
UserAccountKeysData userAccountKeys = null;
|
||||
// JIT TDE users and some broken/old users may not have a private key.
|
||||
@@ -130,7 +137,7 @@ public class SyncController : Controller
|
||||
|
||||
var response = new SyncResponseModel(_globalSettings, user, userAccountKeys, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,
|
||||
organizationIdsClaimingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
|
||||
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
|
||||
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends, webAuthnCredentials);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ using Bit.Api.Models.Response;
|
||||
using Bit.Api.Tools.Models.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Api.Response;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Api.Response;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
@@ -39,7 +42,8 @@ public class SyncResponseModel() : ResponseModel("sync")
|
||||
IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersDict,
|
||||
bool excludeDomains,
|
||||
IEnumerable<Policy> policies,
|
||||
IEnumerable<Send> sends)
|
||||
IEnumerable<Send> sends,
|
||||
IEnumerable<WebAuthnCredential> webAuthnCredentials)
|
||||
: this()
|
||||
{
|
||||
Profile = new ProfileResponseModel(user, userAccountKeysData, organizationUserDetails, providerUserDetails,
|
||||
@@ -57,6 +61,16 @@ public class SyncResponseModel() : ResponseModel("sync")
|
||||
Domains = excludeDomains ? null : new DomainsResponseModel(user, false);
|
||||
Policies = policies?.Select(p => new PolicyResponseModel(p)) ?? new List<PolicyResponseModel>();
|
||||
Sends = sends.Select(s => new SendResponseModel(s));
|
||||
var webAuthnPrfOptions = webAuthnCredentials
|
||||
.Where(c => c.GetPrfStatus() == WebAuthnPrfStatus.Enabled)
|
||||
.Select(c => new WebAuthnPrfDecryptionOption(
|
||||
c.EncryptedPrivateKey,
|
||||
c.EncryptedUserKey,
|
||||
c.CredentialId,
|
||||
[] // transports as empty array
|
||||
))
|
||||
.ToArray();
|
||||
|
||||
UserDecryption = new UserDecryptionResponseModel
|
||||
{
|
||||
MasterPasswordUnlock = user.HasMasterPassword()
|
||||
@@ -72,7 +86,8 @@ public class SyncResponseModel() : ResponseModel("sync")
|
||||
MasterKeyEncryptedUserKey = user.Key!,
|
||||
Salt = user.Email.ToLowerInvariant()
|
||||
}
|
||||
: null
|
||||
: null,
|
||||
WebAuthnPrfOptions = webAuthnPrfOptions.Length > 0 ? webAuthnPrfOptions : null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace Bit.Billing.Services.Implementations;
|
||||
|
||||
public class StripeEventService(
|
||||
GlobalSettings globalSettings,
|
||||
ILogger<StripeEventService> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IProviderRepository providerRepository,
|
||||
ISetupIntentCache setupIntentCache,
|
||||
@@ -148,26 +149,36 @@ public class StripeEventService(
|
||||
{
|
||||
var setupIntent = await GetSetupIntent(localStripeEvent);
|
||||
|
||||
logger.LogInformation("Extracted Setup Intent ({SetupIntentId}) from Stripe 'setup_intent.succeeded' event", setupIntent.Id);
|
||||
|
||||
var subscriberId = await setupIntentCache.GetSubscriberIdForSetupIntent(setupIntent.Id);
|
||||
|
||||
logger.LogInformation("Retrieved subscriber ID ({SubscriberId}) from cache for Setup Intent ({SetupIntentId})", subscriberId, setupIntent.Id);
|
||||
|
||||
if (subscriberId == null)
|
||||
{
|
||||
logger.LogError("Cached subscriber ID for Setup Intent ({SetupIntentId}) is null", setupIntent.Id);
|
||||
return null;
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(subscriberId.Value);
|
||||
logger.LogInformation("Retrieved organization ({OrganizationId}) via subscriber ID for Setup Intent ({SetupIntentId})", organization?.Id, setupIntent.Id);
|
||||
if (organization is { GatewayCustomerId: not null })
|
||||
{
|
||||
var organizationCustomer = await stripeFacade.GetCustomer(organization.GatewayCustomerId);
|
||||
logger.LogInformation("Retrieved customer ({CustomerId}) via organization ID for Setup Intent ({SetupIntentId})", organization.Id, setupIntent.Id);
|
||||
return organizationCustomer.Metadata;
|
||||
}
|
||||
|
||||
var provider = await providerRepository.GetByIdAsync(subscriberId.Value);
|
||||
logger.LogInformation("Retrieved provider ({ProviderId}) via subscriber ID for Setup Intent ({SetupIntentId})", provider?.Id, setupIntent.Id);
|
||||
if (provider is not { GatewayCustomerId: not null })
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var providerCustomer = await stripeFacade.GetCustomer(provider.GatewayCustomerId);
|
||||
logger.LogInformation("Retrieved customer ({CustomerId}) via provider ID for Setup Intent ({SetupIntentId})", provider.Id, setupIntent.Id);
|
||||
return providerCustomer.Metadata;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
public class MasterPasswordPolicyData : IPolicyDataModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimum password complexity score (0-4). Null indicates no complexity requirement.
|
||||
/// </summary>
|
||||
[JsonPropertyName("minComplexity")]
|
||||
[Range(0, 4)]
|
||||
public int? MinComplexity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum password length (12-128). Null indicates no minimum length requirement.
|
||||
/// </summary>
|
||||
[JsonPropertyName("minLength")]
|
||||
[Range(12, 128)]
|
||||
public int? MinLength { get; set; }
|
||||
[JsonPropertyName("requireLower")]
|
||||
public bool? RequireLower { get; set; }
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
public class PolicyStatus
|
||||
{
|
||||
public PolicyStatus(Guid organizationId, PolicyType policyType, Policy? policy = null)
|
||||
{
|
||||
OrganizationId = policy?.OrganizationId ?? organizationId;
|
||||
Data = policy?.Data;
|
||||
Type = policy?.Type ?? policyType;
|
||||
Enabled = policy?.Enabled ?? false;
|
||||
}
|
||||
|
||||
public Guid OrganizationId { get; set; }
|
||||
public PolicyType Type { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public string? Data { get; set; }
|
||||
|
||||
public T GetDataModel<T>() where T : IPolicyDataModel, new()
|
||||
{
|
||||
return CoreHelpers.LoadClassFromJsonData<T>(Data);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
|
||||
|
||||
public class AdminRecoverAccountCommand(IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
IPolicyQuery policyQuery,
|
||||
IUserRepository userRepository,
|
||||
IMailService mailService,
|
||||
IEventService eventService,
|
||||
@@ -30,9 +30,8 @@ public class AdminRecoverAccountCommand(IOrganizationRepository organizationRepo
|
||||
}
|
||||
|
||||
// Enterprise policy must be enabled
|
||||
var resetPasswordPolicy =
|
||||
await policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
||||
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled)
|
||||
var resetPasswordPolicy = await policyQuery.RunAsync(orgId, PolicyType.ResetPassword);
|
||||
if (!resetPasswordPolicy.Enabled)
|
||||
{
|
||||
throw new BadRequestException("Organization does not have the password reset policy enabled.");
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimed
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
@@ -20,7 +19,7 @@ public class AutomaticallyConfirmOrganizationUsersValidator(
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator,
|
||||
IUserService userService,
|
||||
IPolicyRepository policyRepository) : IAutomaticallyConfirmOrganizationUsersValidator
|
||||
IPolicyQuery policyQuery) : IAutomaticallyConfirmOrganizationUsersValidator
|
||||
{
|
||||
public async Task<ValidationResult<AutomaticallyConfirmOrganizationUserValidationRequest>> ValidateAsync(
|
||||
AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
@@ -74,7 +73,7 @@ public class AutomaticallyConfirmOrganizationUsersValidator(
|
||||
}
|
||||
|
||||
private async Task<bool> OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(AutomaticallyConfirmOrganizationUserValidationRequest request) =>
|
||||
await policyRepository.GetByOrganizationIdTypeAsync(request.OrganizationId, PolicyType.AutomaticUserConfirmation) is { Enabled: true }
|
||||
(await policyQuery.RunAsync(request.OrganizationId, PolicyType.AutomaticUserConfirmation)).Enabled
|
||||
&& request.Organization is { UseAutomaticUserConfirmation: true };
|
||||
|
||||
private async Task<bool> OrganizationUserConformsToTwoFactorRequiredPolicyAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.Auth.Models.Business;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
@@ -19,7 +19,7 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUse
|
||||
public class SendOrganizationInvitesCommand(
|
||||
IUserRepository userRepository,
|
||||
ISsoConfigRepository ssoConfigurationRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
IPolicyQuery policyQuery,
|
||||
IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory,
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> dataProtectorTokenFactory,
|
||||
IMailService mailService) : ISendOrganizationInvitesCommand
|
||||
@@ -58,7 +58,7 @@ public class SendOrganizationInvitesCommand(
|
||||
// need to check the policy if the org has SSO enabled.
|
||||
var orgSsoLoginRequiredPolicyEnabled = orgSsoEnabled &&
|
||||
organization.UsePolicies &&
|
||||
(await policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.RequireSso))?.Enabled == true;
|
||||
(await policyQuery.RunAsync(organization.Id, PolicyType.RequireSso)).Enabled;
|
||||
|
||||
// Generate the list of org users and expiring tokens
|
||||
// create helper function to create expiring tokens
|
||||
|
||||
@@ -20,7 +20,7 @@ public interface IRestoreOrganizationUserCommand
|
||||
/// </summary>
|
||||
/// <param name="organizationUser">Revoked user to be restored.</param>
|
||||
/// <param name="restoringUserId">UserId of the user performing the action.</param>
|
||||
Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId);
|
||||
Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId, string? defaultCollectionName);
|
||||
|
||||
/// <summary>
|
||||
/// Validates that the requesting user can perform the action. There is also a check done to ensure the organization
|
||||
@@ -50,5 +50,5 @@ public interface IRestoreOrganizationUserCommand
|
||||
/// <param name="userService">Passed in from caller to avoid circular dependency</param>
|
||||
/// <returns>List of organization user Ids and strings. A successful restoration will have an empty string.
|
||||
/// If an error occurs, the error message will be provided.</returns>
|
||||
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
|
||||
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService, string? defaultCollectionName);
|
||||
}
|
||||
|
||||
@@ -31,9 +31,10 @@ public class RestoreOrganizationUserCommand(
|
||||
IOrganizationService organizationService,
|
||||
IFeatureService featureService,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
ICollectionRepository collectionRepository,
|
||||
IAutomaticUserConfirmationPolicyEnforcementValidator automaticUserConfirmationPolicyEnforcementValidator) : IRestoreOrganizationUserCommand
|
||||
{
|
||||
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId)
|
||||
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId, string defaultCollectionName)
|
||||
{
|
||||
if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId.Value)
|
||||
{
|
||||
@@ -46,7 +47,7 @@ public class RestoreOrganizationUserCommand(
|
||||
throw new BadRequestException("Only owners can restore other owners.");
|
||||
}
|
||||
|
||||
await RepositoryRestoreUserAsync(organizationUser);
|
||||
await RepositoryRestoreUserAsync(organizationUser, defaultCollectionName);
|
||||
await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
|
||||
if (organizationUser.UserId.HasValue)
|
||||
@@ -57,7 +58,7 @@ public class RestoreOrganizationUserCommand(
|
||||
|
||||
public async Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser)
|
||||
{
|
||||
await RepositoryRestoreUserAsync(organizationUser);
|
||||
await RepositoryRestoreUserAsync(organizationUser, null); // users stored by a system user will not get a default collection at this point.
|
||||
await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored,
|
||||
systemUser);
|
||||
|
||||
@@ -67,7 +68,7 @@ public class RestoreOrganizationUserCommand(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser)
|
||||
private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser, string defaultCollectionName)
|
||||
{
|
||||
if (organizationUser.Status != OrganizationUserStatusType.Revoked)
|
||||
{
|
||||
@@ -104,7 +105,17 @@ public class RestoreOrganizationUserCommand(
|
||||
|
||||
await organizationUserRepository.RestoreAsync(organizationUser.Id, status);
|
||||
|
||||
organizationUser.Status = status;
|
||||
if (organizationUser.UserId.HasValue
|
||||
&& (await policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(organizationUser.UserId
|
||||
.Value)).State == OrganizationDataOwnershipState.Enabled
|
||||
&& status == OrganizationUserStatusType.Confirmed
|
||||
&& featureService.IsEnabled(FeatureFlagKeys.DefaultUserCollectionRestore)
|
||||
&& !string.IsNullOrWhiteSpace(defaultCollectionName))
|
||||
{
|
||||
await collectionRepository.CreateDefaultCollectionsAsync(organizationUser.OrganizationId,
|
||||
[organizationUser.Id],
|
||||
defaultCollectionName);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckUserForOtherFreeOrganizationOwnershipAsync(OrganizationUser organizationUser)
|
||||
@@ -156,7 +167,8 @@ public class RestoreOrganizationUserCommand(
|
||||
}
|
||||
|
||||
public async Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
|
||||
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService)
|
||||
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
var orgUsers = await organizationUserRepository.GetManyAsync(organizationUserIds);
|
||||
var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId)
|
||||
@@ -187,6 +199,9 @@ public class RestoreOrganizationUserCommand(
|
||||
var orgUsersAndOrgs = await GetRelatedOrganizationUsersAndOrganizationsAsync(filteredUsers);
|
||||
|
||||
var result = new List<Tuple<OrganizationUser, string>>();
|
||||
var organizationUsersDataOwnershipEnabled = (await policyRequirementQuery
|
||||
.GetManyByOrganizationIdAsync<OrganizationDataOwnershipPolicyRequirement>(organizationId))
|
||||
.ToList();
|
||||
|
||||
foreach (var organizationUser in filteredUsers)
|
||||
{
|
||||
@@ -223,13 +238,24 @@ public class RestoreOrganizationUserCommand(
|
||||
var status = OrganizationService.GetPriorActiveOrganizationUserStatusType(organizationUser);
|
||||
|
||||
await organizationUserRepository.RestoreAsync(organizationUser.Id, status);
|
||||
organizationUser.Status = status;
|
||||
await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
|
||||
if (organizationUser.UserId.HasValue)
|
||||
{
|
||||
if (organizationUsersDataOwnershipEnabled.Contains(organizationUser.Id)
|
||||
&& status == OrganizationUserStatusType.Confirmed
|
||||
&& !string.IsNullOrWhiteSpace(defaultCollectionName)
|
||||
&& featureService.IsEnabled(FeatureFlagKeys.DefaultUserCollectionRestore))
|
||||
{
|
||||
await collectionRepository.CreateDefaultCollectionsAsync(organizationUser.OrganizationId,
|
||||
[organizationUser.Id],
|
||||
defaultCollectionName);
|
||||
}
|
||||
|
||||
await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||
}
|
||||
|
||||
await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
|
||||
result.Add(Tuple.Create(organizationUser, ""));
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
|
||||
public interface IPolicyQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a summary view of an organization's usage of a policy specified by the <paramref name="policyType"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This query is the entrypoint for consumers interested in understanding how a particular <see cref="PolicyType"/>
|
||||
/// has been applied to an organization; the resultant <see cref="PolicyStatus"/> is not indicative of explicit
|
||||
/// policy configuration.
|
||||
/// </remarks>
|
||||
Task<PolicyStatus> RunAsync(Guid organizationId, PolicyType policyType);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||
|
||||
public class PolicyQuery(IPolicyRepository policyRepository) : IPolicyQuery
|
||||
{
|
||||
public async Task<PolicyStatus> RunAsync(Guid organizationId, PolicyType policyType)
|
||||
{
|
||||
var dbPolicy = await policyRepository.GetByOrganizationIdTypeAsync(organizationId, policyType);
|
||||
return new PolicyStatus(organizationId, policyType, dbPolicy);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements
|
||||
/// <param name="policyDetails">Collection of policy details that apply to this user id</param>
|
||||
public class AutomaticUserConfirmationPolicyRequirement(IEnumerable<PolicyDetails> policyDetails) : IPolicyRequirement
|
||||
{
|
||||
public bool CannotBeGrantedEmergencyAccess() => policyDetails.Any();
|
||||
public bool CannotHaveEmergencyAccess() => policyDetails.Any();
|
||||
|
||||
public bool CannotJoinProvider() => policyDetails.Any();
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ public static class PolicyServiceCollectionExtensions
|
||||
services.AddScoped<ISavePolicyCommand, SavePolicyCommand>();
|
||||
services.AddScoped<IVNextSavePolicyCommand, VNextSavePolicyCommand>();
|
||||
services.AddScoped<IPolicyRequirementQuery, PolicyRequirementQuery>();
|
||||
services.AddScoped<IPolicyQuery, PolicyQuery>();
|
||||
services.AddScoped<IPolicyEventHandlerFactory, PolicyEventHandlerHandlerFactory>();
|
||||
|
||||
services.AddPolicyValidators();
|
||||
|
||||
@@ -27,7 +27,6 @@ public interface IOrganizationService
|
||||
OrganizationUserInvite invite, string externalId);
|
||||
Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser,
|
||||
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites);
|
||||
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
|
||||
Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId);
|
||||
Task DeleteSsoUserAsync(Guid userId, Guid? organizationId);
|
||||
Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null);
|
||||
|
||||
@@ -14,7 +14,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.AdminConsole.Utilities.DebuggingInstruments;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
@@ -49,7 +48,7 @@ public class OrganizationService : IOrganizationService
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IStripePaymentService _paymentService;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IPolicyQuery _policyQuery;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly ISsoUserRepository _ssoUserRepository;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
@@ -76,7 +75,7 @@ public class OrganizationService : IOrganizationService
|
||||
IEventService eventService,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IStripePaymentService paymentService,
|
||||
IPolicyRepository policyRepository,
|
||||
IPolicyQuery policyQuery,
|
||||
IPolicyService policyService,
|
||||
ISsoUserRepository ssoUserRepository,
|
||||
IGlobalSettings globalSettings,
|
||||
@@ -103,7 +102,7 @@ public class OrganizationService : IOrganizationService
|
||||
_eventService = eventService;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_paymentService = paymentService;
|
||||
_policyRepository = policyRepository;
|
||||
_policyQuery = policyQuery;
|
||||
_policyService = policyService;
|
||||
_ssoUserRepository = ssoUserRepository;
|
||||
_globalSettings = globalSettings;
|
||||
@@ -718,32 +717,6 @@ public class OrganizationService : IOrganizationService
|
||||
return (allOrgUsers, events);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId,
|
||||
Guid? invitingUserId,
|
||||
IEnumerable<Guid> organizationUsersId)
|
||||
{
|
||||
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);
|
||||
_logger.LogUserInviteStateDiagnostics(orgUsers);
|
||||
|
||||
var org = await GetOrgById(organizationId);
|
||||
|
||||
var result = new List<Tuple<OrganizationUser, string>>();
|
||||
foreach (var orgUser in orgUsers)
|
||||
{
|
||||
if (orgUser.Status != OrganizationUserStatusType.Invited || orgUser.OrganizationId != organizationId)
|
||||
{
|
||||
result.Add(Tuple.Create(orgUser, "User invalid."));
|
||||
continue;
|
||||
}
|
||||
|
||||
await SendInviteAsync(orgUser, org, false);
|
||||
result.Add(Tuple.Create(orgUser, ""));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private async Task SendInvitesAsync(IEnumerable<OrganizationUser> orgUsers, Organization organization) =>
|
||||
await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest(orgUsers, organization));
|
||||
|
||||
@@ -862,9 +835,8 @@ public class OrganizationService : IOrganizationService
|
||||
}
|
||||
|
||||
// Make sure the organization has the policy enabled
|
||||
var resetPasswordPolicy =
|
||||
await _policyRepository.GetByOrganizationIdTypeAsync(organizationId, PolicyType.ResetPassword);
|
||||
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled)
|
||||
var resetPasswordPolicy = await _policyQuery.RunAsync(organizationId, PolicyType.ResetPassword);
|
||||
if (!resetPasswordPolicy.Enabled)
|
||||
{
|
||||
throw new BadRequestException("Organization does not have the password reset policy enabled.");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
@@ -30,7 +31,8 @@ public static class PolicyDataValidator
|
||||
switch (policyType)
|
||||
{
|
||||
case PolicyType.MasterPassword:
|
||||
CoreHelpers.LoadClassFromJsonData<MasterPasswordPolicyData>(json);
|
||||
var masterPasswordData = CoreHelpers.LoadClassFromJsonData<MasterPasswordPolicyData>(json);
|
||||
ValidateModel(masterPasswordData, policyType);
|
||||
break;
|
||||
case PolicyType.SendOptions:
|
||||
CoreHelpers.LoadClassFromJsonData<SendOptionsPolicyData>(json);
|
||||
@@ -44,11 +46,24 @@ public static class PolicyDataValidator
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var fieldInfo = !string.IsNullOrEmpty(ex.Path) ? $": field '{ex.Path}' has invalid type" : "";
|
||||
var fieldName = !string.IsNullOrEmpty(ex.Path) ? ex.Path.TrimStart('$', '.') : null;
|
||||
var fieldInfo = !string.IsNullOrEmpty(fieldName) ? $": {fieldName} has an invalid value" : "";
|
||||
throw new BadRequestException($"Invalid data for {policyType} policy{fieldInfo}.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateModel(object model, PolicyType policyType)
|
||||
{
|
||||
var validationContext = new ValidationContext(model);
|
||||
var validationResults = new List<ValidationResult>();
|
||||
|
||||
if (!Validator.TryValidateObject(model, validationContext, validationResults, true))
|
||||
{
|
||||
var errors = string.Join(", ", validationResults.Select(r => r.ErrorMessage));
|
||||
throw new BadRequestException($"Invalid data for {policyType} policy: {errors}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates and deserializes policy metadata based on the policy type.
|
||||
/// </summary>
|
||||
|
||||
@@ -45,13 +45,19 @@ public class WebAuthnPrfDecryptionOption
|
||||
{
|
||||
public string EncryptedPrivateKey { get; }
|
||||
public string EncryptedUserKey { get; }
|
||||
public string CredentialId { get; }
|
||||
public string[] Transports { get; }
|
||||
|
||||
public WebAuthnPrfDecryptionOption(
|
||||
string encryptedPrivateKey,
|
||||
string encryptedUserKey)
|
||||
string encryptedUserKey,
|
||||
string credentialId,
|
||||
string[]? transports = null)
|
||||
{
|
||||
EncryptedPrivateKey = encryptedPrivateKey;
|
||||
EncryptedUserKey = encryptedUserKey;
|
||||
CredentialId = credentialId;
|
||||
Transports = transports ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
@@ -21,7 +21,7 @@ namespace Bit.Core.Auth.Services;
|
||||
public class SsoConfigService : ISsoConfigService
|
||||
{
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IPolicyQuery _policyQuery;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IEventService _eventService;
|
||||
@@ -29,14 +29,14 @@ public class SsoConfigService : ISsoConfigService
|
||||
|
||||
public SsoConfigService(
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
IPolicyQuery policyQuery,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IEventService eventService,
|
||||
IVNextSavePolicyCommand vNextSavePolicyCommand)
|
||||
{
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_policyRepository = policyRepository;
|
||||
_policyQuery = policyQuery;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_eventService = eventService;
|
||||
@@ -114,14 +114,14 @@ public class SsoConfigService : ISsoConfigService
|
||||
throw new BadRequestException("Organization cannot use Key Connector.");
|
||||
}
|
||||
|
||||
var singleOrgPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.SingleOrg);
|
||||
if (singleOrgPolicy is not { Enabled: true })
|
||||
var singleOrgPolicy = await _policyQuery.RunAsync(config.OrganizationId, PolicyType.SingleOrg);
|
||||
if (!singleOrgPolicy.Enabled)
|
||||
{
|
||||
throw new BadRequestException("Key Connector requires the Single Organization policy to be enabled.");
|
||||
}
|
||||
|
||||
var ssoPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.RequireSso);
|
||||
if (ssoPolicy is not { Enabled: true })
|
||||
var ssoPolicy = await _policyQuery.RunAsync(config.OrganizationId, PolicyType.RequireSso);
|
||||
if (!ssoPolicy.Enabled)
|
||||
{
|
||||
throw new BadRequestException("Key Connector requires the Single Sign-On Authentication policy to be enabled.");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
@@ -27,7 +27,7 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IPolicyQuery _policyQuery;
|
||||
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
@@ -50,7 +50,7 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
IGlobalSettings globalSettings,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPolicyRepository policyRepository,
|
||||
IPolicyQuery policyQuery,
|
||||
IOrganizationDomainRepository organizationDomainRepository,
|
||||
IFeatureService featureService,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
@@ -65,7 +65,7 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
_globalSettings = globalSettings;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_policyRepository = policyRepository;
|
||||
_policyQuery = policyQuery;
|
||||
_organizationDomainRepository = organizationDomainRepository;
|
||||
_featureService = featureService;
|
||||
|
||||
@@ -246,9 +246,9 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
var orgUser = await _organizationUserRepository.GetByIdAsync(orgUserId.Value);
|
||||
if (orgUser != null)
|
||||
{
|
||||
var twoFactorPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(orgUser.OrganizationId,
|
||||
var twoFactorPolicy = await _policyQuery.RunAsync(orgUser.OrganizationId,
|
||||
PolicyType.TwoFactorAuthentication);
|
||||
if (twoFactorPolicy != null && twoFactorPolicy.Enabled)
|
||||
if (twoFactorPolicy.Enabled)
|
||||
{
|
||||
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
|
||||
{
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Billing.Caches.Implementations;
|
||||
|
||||
public class SetupIntentDistributedCache(
|
||||
[FromKeyedServices("persistent")]
|
||||
IDistributedCache distributedCache) : ISetupIntentCache
|
||||
IDistributedCache distributedCache,
|
||||
ILogger<SetupIntentDistributedCache> logger) : ISetupIntentCache
|
||||
{
|
||||
public async Task<string?> GetSetupIntentIdForSubscriber(Guid subscriberId)
|
||||
{
|
||||
@@ -17,11 +19,12 @@ public class SetupIntentDistributedCache(
|
||||
{
|
||||
var cacheKey = GetCacheKeyBySetupIntentId(setupIntentId);
|
||||
var value = await distributedCache.GetStringAsync(cacheKey);
|
||||
if (string.IsNullOrEmpty(value) || !Guid.TryParse(value, out var subscriberId))
|
||||
if (!string.IsNullOrEmpty(value) && Guid.TryParse(value, out var subscriberId))
|
||||
{
|
||||
return null;
|
||||
return subscriberId;
|
||||
}
|
||||
return subscriberId;
|
||||
logger.LogError("Subscriber ID value ({Value}) cached for Setup Intent ({SetupIntentId}) is null or not a valid Guid", value, setupIntentId);
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task RemoveSetupIntentForSubscriber(Guid subscriberId)
|
||||
|
||||
@@ -94,6 +94,8 @@ public class UpdatePaymentMethodCommand(
|
||||
|
||||
await setupIntentCache.Set(subscriber.Id, setupIntent.Id);
|
||||
|
||||
_logger.LogInformation("{Command}: Successfully cached Setup Intent ({SetupIntentId}) for subscriber ({SubscriberID})", CommandName, setupIntent.Id, subscriber.Id);
|
||||
|
||||
await UnlinkBraintreeCustomerAsync(customer);
|
||||
|
||||
return MaskedPaymentMethod.From(setupIntent);
|
||||
|
||||
@@ -141,7 +141,7 @@ public static class FeatureFlagKeys
|
||||
public const string AutomaticConfirmUsers = "pm-19934-auto-confirm-organization-users";
|
||||
public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache";
|
||||
public const string BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration";
|
||||
public const string IncreaseBulkReinviteLimitForCloud = "pm-28251-increase-bulk-reinvite-limit-for-cloud";
|
||||
public const string DefaultUserCollectionRestore = "pm-30883-my-items-restored-users";
|
||||
public const string PremiumAccessQuery = "pm-29495-refactor-premium-interface";
|
||||
public const string RefactorMembersComponent = "pm-29503-refactor-members-inheritance";
|
||||
|
||||
@@ -149,6 +149,7 @@ public static class FeatureFlagKeys
|
||||
public const string DesktopMigrationMilestone1 = "desktop-ui-migration-milestone-1";
|
||||
public const string DesktopMigrationMilestone2 = "desktop-ui-migration-milestone-2";
|
||||
public const string DesktopMigrationMilestone3 = "desktop-ui-migration-milestone-3";
|
||||
public const string DesktopMigrationMilestone4 = "desktop-ui-migration-milestone-4";
|
||||
|
||||
/* Auth Team */
|
||||
public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence";
|
||||
@@ -160,10 +161,12 @@ public static class FeatureFlagKeys
|
||||
public const string PM24579_PreventSsoOnExistingNonCompliantUsers = "pm-24579-prevent-sso-on-existing-non-compliant-users";
|
||||
public const string DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods";
|
||||
public const string MJMLBasedEmailTemplates = "mjml-based-email-templates";
|
||||
public const string PM2035PasskeyUnlock = "pm-2035-passkey-unlock";
|
||||
public const string MjmlWelcomeEmailTemplates = "pm-21741-mjml-welcome-email";
|
||||
public const string OrganizationConfirmationEmail = "pm-28402-update-confirmed-to-org-email-template";
|
||||
public const string MarketingInitiatedPremiumFlow = "pm-26140-marketing-initiated-premium-flow";
|
||||
public const string PrefetchPasswordPrelogin = "pm-23801-prefetch-password-prelogin";
|
||||
public const string SafariAccountSwitching = "pm-5594-safari-account-switching";
|
||||
public const string PM27086_UpdateAuthenticationApisForInputPassword = "pm-27086-update-authentication-apis-for-input-password";
|
||||
|
||||
/* Autofill Team */
|
||||
@@ -174,6 +177,7 @@ public static class FeatureFlagKeys
|
||||
public const string MacOsNativeCredentialSync = "macos-native-credential-sync";
|
||||
public const string WindowsDesktopAutotype = "windows-desktop-autotype";
|
||||
public const string WindowsDesktopAutotypeGA = "windows-desktop-autotype-ga";
|
||||
public const string NotificationUndeterminedCipherScenarioLogic = "undetermined-cipher-scenario-logic";
|
||||
|
||||
/* Billing Team */
|
||||
public const string TrialPayment = "PM-8163-trial-payment";
|
||||
@@ -221,9 +225,10 @@ public static class FeatureFlagKeys
|
||||
|
||||
/* Platform Team */
|
||||
public const string WebPush = "web-push";
|
||||
public const string IpcChannelFramework = "ipc-channel-framework";
|
||||
public const string ContentScriptIpcFramework = "content-script-ipc-channel-framework";
|
||||
public const string PushNotificationsWhenLocked = "pm-19388-push-notifications-when-locked";
|
||||
public const string PushNotificationsWhenInactive = "pm-25130-receive-push-notifications-for-inactive-users";
|
||||
public const string WebAuthnRelatedOrigins = "pm-30529-webauthn-related-origins";
|
||||
|
||||
/* Tools Team */
|
||||
/// <summary>
|
||||
@@ -255,6 +260,8 @@ public static class FeatureFlagKeys
|
||||
/* DIRT Team */
|
||||
public const string EventManagementForDataDogAndCrowdStrike = "event-management-for-datadog-and-crowdstrike";
|
||||
public const string EventDiagnosticLogging = "pm-27666-siem-event-log-debugging";
|
||||
public const string EventManagementForHuntress = "event-management-for-huntress";
|
||||
public const string Milestone11AppPageImprovements = "pm-30538-dirt-milestone-11-app-page-improvements";
|
||||
|
||||
/* UIF Team */
|
||||
public const string RouterFocusManagement = "router-focus-management";
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.10" />
|
||||
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.10.4" />
|
||||
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.11.0" />
|
||||
<PackageReference Include="Quartz" Version="3.15.1" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.1" />
|
||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.15.1" />
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
namespace Bit.Core.KeyManagement.Models.Api.Response;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Auth.Models.Api.Response;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Api.Response;
|
||||
|
||||
public class UserDecryptionResponseModel
|
||||
{
|
||||
@@ -6,4 +9,10 @@ public class UserDecryptionResponseModel
|
||||
/// Returns the unlock data when the user has a master password that can be used to decrypt their vault.
|
||||
/// </summary>
|
||||
public MasterPasswordUnlockResponseModel? MasterPasswordUnlock { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WebAuthn PRF decryption keys.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public WebAuthnPrfDecryptionOption[]? WebAuthnPrfOptions { get; set; }
|
||||
}
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="display: table; width:100%; padding: 30px; text-align: left;" align="center">
|
||||
<tr>
|
||||
<td style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 16px; line-height: 24px; margin-top: 30px; margin-bottom: 25px; margin-left: 35px; margin-right: 35px;">
|
||||
<b>Here's what that means:</b>
|
||||
<b>What this means for you</b>
|
||||
<ul>
|
||||
<li>Your Bitwarden account is owned by {{OrganizationName}}</li>
|
||||
<li>Your administrators can delete your account at any time</li>
|
||||
<li>You cannot leave the organization</li>
|
||||
<li>Your day-to-day use of Bitwarden remains the same.</li>
|
||||
<li>Only store work-related items in your {{OrganizationName}} vault.</li>
|
||||
<li>{{OrganizationName}} admins now manage your account, meaning they can revoke or delete your account.</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 16px; line-height: 24px; margin-top: 30px; margin-bottom: 25px; margin-left: 35px; margin-right: 35px;">
|
||||
For more information, please refer to the following help article: <a href="https://bitwarden.com/help/claimed-accounts">Claimed Accounts</a>
|
||||
For more information, please refer to the following help article: <a href="https://bitwarden.com/help/claimed-accounts">Claimed accounts</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
As a member of {{OrganizationName}}, your Bitwarden account is claimed and owned by your organization.
|
||||
What this means for you:
|
||||
- Your day-to-day use of Bitwarden remains the same.
|
||||
- Only store work-related items in your {{OrganizationName}} vault.
|
||||
- {{OrganizationName}} admins now manage your account, meaning they can revoke or delete your account.
|
||||
|
||||
Here's what that means:
|
||||
- Your administrators can delete your account at any time
|
||||
- You cannot leave the organization
|
||||
|
||||
For more information, please refer to the following help article: Claimed Accounts (https://bitwarden.com/help/claimed-accounts)
|
||||
For more information, please refer to the following help article: Claimed accounts (https://bitwarden.com/help/claimed-accounts)
|
||||
|
||||
@@ -5,6 +5,7 @@ using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
@@ -30,6 +31,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IStripePaymentService _paymentService;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IPolicyQuery _policyQuery;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
|
||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||
@@ -45,6 +47,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
||||
IGroupRepository groupRepository,
|
||||
IStripePaymentService paymentService,
|
||||
IPolicyRepository policyRepository,
|
||||
IPolicyQuery policyQuery,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IOrganizationConnectionRepository organizationConnectionRepository,
|
||||
IServiceAccountRepository serviceAccountRepository,
|
||||
@@ -59,6 +62,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
||||
_groupRepository = groupRepository;
|
||||
_paymentService = paymentService;
|
||||
_policyRepository = policyRepository;
|
||||
_policyQuery = policyQuery;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_organizationConnectionRepository = organizationConnectionRepository;
|
||||
_serviceAccountRepository = serviceAccountRepository;
|
||||
@@ -184,9 +188,8 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
||||
|
||||
if (!newPlan.HasResetPassword && organization.UseResetPassword)
|
||||
{
|
||||
var resetPasswordPolicy =
|
||||
await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
|
||||
if (resetPasswordPolicy != null && resetPasswordPolicy.Enabled)
|
||||
var resetPasswordPolicy = await _policyQuery.RunAsync(organization.Id, PolicyType.ResetPassword);
|
||||
if (resetPasswordPolicy.Enabled)
|
||||
{
|
||||
throw new BadRequestException("Your new plan does not allow the Password Reset feature. " +
|
||||
"Disable your Password Reset policy.");
|
||||
|
||||
@@ -656,11 +656,11 @@ public class HandlebarsMailService : IMailService
|
||||
return;
|
||||
|
||||
MailQueueMessage CreateMessage(string emailAddress, Organization org) =>
|
||||
new(CreateDefaultMessage($"Your Bitwarden account is claimed by {org.DisplayName()}", emailAddress),
|
||||
new(CreateDefaultMessage($"Important update to your Bitwarden account", emailAddress),
|
||||
"AdminConsole.DomainClaimedByOrganization",
|
||||
new ClaimedDomainUserNotificationViewModel
|
||||
{
|
||||
TitleFirst = $"Your Bitwarden account is claimed by {org.DisplayName()}",
|
||||
TitleFirst = $"Important update to your Bitwarden account",
|
||||
OrganizationName = CoreHelpers.SanitizeForEmail(org.DisplayName(), false)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ public class UserService : UserManager<User>, IUserService
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IStripePaymentService _paymentService;
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IPolicyQuery _policyQuery;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IFido2 _fido2;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
@@ -98,7 +98,7 @@ public class UserService : UserManager<User>, IUserService
|
||||
IEventService eventService,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IStripePaymentService paymentService,
|
||||
IPolicyRepository policyRepository,
|
||||
IPolicyQuery policyQuery,
|
||||
IPolicyService policyService,
|
||||
IFido2 fido2,
|
||||
ICurrentContext currentContext,
|
||||
@@ -139,7 +139,7 @@ public class UserService : UserManager<User>, IUserService
|
||||
_eventService = eventService;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_paymentService = paymentService;
|
||||
_policyRepository = policyRepository;
|
||||
_policyQuery = policyQuery;
|
||||
_policyService = policyService;
|
||||
_fido2 = fido2;
|
||||
_currentContext = currentContext;
|
||||
@@ -722,9 +722,8 @@ public class UserService : UserManager<User>, IUserService
|
||||
}
|
||||
|
||||
// Enterprise policy must be enabled
|
||||
var resetPasswordPolicy =
|
||||
await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
||||
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled)
|
||||
var resetPasswordPolicy = await _policyQuery.RunAsync(orgId, PolicyType.ResetPassword);
|
||||
if (!resetPasswordPolicy.Enabled)
|
||||
{
|
||||
throw new BadRequestException("Organization does not have the password reset policy enabled.");
|
||||
}
|
||||
|
||||
@@ -81,6 +81,15 @@ public class Send : ITableObject<Guid>
|
||||
[MaxLength(4000)]
|
||||
public string? Emails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Comma-separated list of email **hashes** for OTP authentication.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This field is mutually exclusive with <see cref="Password" />
|
||||
/// </remarks>
|
||||
[MaxLength(4000)]
|
||||
public string? EmailHashes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The send becomes unavailable to API callers when
|
||||
/// <see cref="AccessCount"/> >= <see cref="MaxAccessCount"/>.
|
||||
|
||||
@@ -44,7 +44,7 @@ public record ResourcePassword(string Hash) : SendAuthenticationMethod;
|
||||
/// <summary>
|
||||
/// Create a send claim by requesting a one time password (OTP) confirmation code.
|
||||
/// </summary>
|
||||
/// <param name="Emails">
|
||||
/// The list of email addresses permitted access to the send.
|
||||
/// <param name="EmailHashes">
|
||||
/// The list of email address **hashes** permitted access to the send.
|
||||
/// </param>
|
||||
public record EmailOtp(string[] Emails) : SendAuthenticationMethod;
|
||||
public record EmailOtp(string[] EmailHashes) : SendAuthenticationMethod;
|
||||
|
||||
@@ -37,8 +37,11 @@ public class SendAuthenticationQuery : ISendAuthenticationQuery
|
||||
SendAuthenticationMethod method = send switch
|
||||
{
|
||||
null => NEVER_AUTHENTICATE,
|
||||
var s when s.AccessCount >= s.MaxAccessCount => NEVER_AUTHENTICATE,
|
||||
var s when s.AuthType == AuthType.Email && s.Emails is not null => emailOtp(s.Emails),
|
||||
var s when s.Disabled => NEVER_AUTHENTICATE,
|
||||
var s when s.AccessCount >= s.MaxAccessCount.GetValueOrDefault(int.MaxValue) => NEVER_AUTHENTICATE,
|
||||
var s when s.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < DateTime.UtcNow => NEVER_AUTHENTICATE,
|
||||
var s when s.DeletionDate <= DateTime.UtcNow => NEVER_AUTHENTICATE,
|
||||
var s when s.AuthType == AuthType.Email && s.EmailHashes is not null => EmailOtp(s.EmailHashes),
|
||||
var s when s.AuthType == AuthType.Password && s.Password is not null => new ResourcePassword(s.Password),
|
||||
_ => NOT_AUTHENTICATED
|
||||
};
|
||||
@@ -46,9 +49,13 @@ public class SendAuthenticationQuery : ISendAuthenticationQuery
|
||||
return method;
|
||||
}
|
||||
|
||||
private EmailOtp emailOtp(string emails)
|
||||
private static EmailOtp EmailOtp(string? emailHashes)
|
||||
{
|
||||
var list = emails.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (string.IsNullOrWhiteSpace(emailHashes))
|
||||
{
|
||||
return new EmailOtp([]);
|
||||
}
|
||||
var list = emailHashes.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
return new EmailOtp(list);
|
||||
}
|
||||
}
|
||||
|
||||
64
src/Core/Utilities/DomainNameAttribute.cs
Normal file
64
src/Core/Utilities/DomainNameAttribute.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Bit.Core.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// https://bitwarden.atlassian.net/browse/VULN-376
|
||||
/// Domain names are vulnerable to XSS attacks if not properly validated.
|
||||
/// Domain names can contain letters, numbers, dots, and hyphens.
|
||||
/// Domain names maybe internationalized (IDN) and contain unicode characters.
|
||||
/// </summary>
|
||||
public class DomainNameValidatorAttribute : ValidationAttribute
|
||||
{
|
||||
// RFC 1123 compliant domain name regex
|
||||
// - Allows alphanumeric characters and hyphens
|
||||
// - Cannot start or end with a hyphen
|
||||
// - Each label (part between dots) must be 1-63 characters
|
||||
// - Total length should not exceed 253 characters
|
||||
// - Supports internationalized domain names (IDN) - which is why this regex includes unicode ranges
|
||||
private static readonly Regex _domainNameRegex = new(
|
||||
@"^(?:[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF](?:[a-zA-Z0-9\-\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{0,61}[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?\.)*[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF](?:[a-zA-Z0-9\-\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{0,61}[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?$",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase
|
||||
);
|
||||
|
||||
public DomainNameValidatorAttribute()
|
||||
: base("The {0} field is not a valid domain name.")
|
||||
{ }
|
||||
|
||||
public override bool IsValid(object? value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return true; // Use [Required] for null checks
|
||||
}
|
||||
|
||||
var domainName = value.ToString();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(domainName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject if contains any whitespace (including leading/trailing spaces, tabs, newlines)
|
||||
if (domainName.Any(char.IsWhiteSpace))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check length constraints
|
||||
if (domainName.Length > 253)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for control characters or other dangerous characters
|
||||
if (domainName.Any(c => char.IsControl(c) || c == '<' || c == '>' || c == '"' || c == '\'' || c == '&'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate against domain name regex
|
||||
return _domainNameRegex.IsMatch(domainName);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Globalization;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Microsoft.IdentityModel.Logging;
|
||||
|
||||
namespace Bit.EventsProcessor;
|
||||
|
||||
@@ -40,7 +39,6 @@ public class Startup
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
IdentityModelEventSource.ShowPII = true;
|
||||
// Add general security headers
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
app.UseRouting();
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Icons' " />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="1.2.0" />
|
||||
<PackageReference Include="AngleSharp" Version="1.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
@@ -40,8 +42,10 @@ public class SendEmailOtpRequestValidator(
|
||||
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailRequired);
|
||||
}
|
||||
|
||||
// email must be in the list of emails in the EmailOtp array
|
||||
if (!authMethod.Emails.Contains(email))
|
||||
// email hash must be in the list of email hashes in the EmailOtp array
|
||||
byte[] hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(email));
|
||||
string hashEmailHex = Convert.ToHexString(hashBytes).ToUpperInvariant();
|
||||
if (!authMethod.EmailHashes.Contains(hashEmailHex))
|
||||
{
|
||||
return BuildErrorResult(SendAccessConstants.EmailOtpValidatorResults.EmailInvalid);
|
||||
}
|
||||
|
||||
@@ -64,8 +64,12 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
|
||||
{
|
||||
if (credential.GetPrfStatus() == WebAuthnPrfStatus.Enabled)
|
||||
{
|
||||
_options.WebAuthnPrfOption =
|
||||
new WebAuthnPrfDecryptionOption(credential.EncryptedPrivateKey, credential.EncryptedUserKey);
|
||||
_options.WebAuthnPrfOption = new WebAuthnPrfDecryptionOption(
|
||||
credential.EncryptedPrivateKey,
|
||||
credential.EncryptedUserKey,
|
||||
credential.CredentialId,
|
||||
[] // Stored credentials currently lack Transports, just send an empty array for now
|
||||
);
|
||||
}
|
||||
|
||||
return this;
|
||||
|
||||
@@ -14,7 +14,6 @@ using Bit.SharedWeb.Swagger;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.IdentityModel.Logging;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
namespace Bit.Identity;
|
||||
@@ -170,16 +169,14 @@ public class Startup
|
||||
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
IWebHostEnvironment env,
|
||||
IWebHostEnvironment environment,
|
||||
GlobalSettings globalSettings,
|
||||
ILogger<Startup> logger)
|
||||
{
|
||||
IdentityModelEventSource.ShowPII = true;
|
||||
|
||||
// Add general security headers
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
|
||||
if (!env.IsDevelopment())
|
||||
if (!environment.IsDevelopment())
|
||||
{
|
||||
var uri = new Uri(globalSettings.BaseServiceUri.Identity);
|
||||
app.Use(async (ctx, next) =>
|
||||
@@ -196,7 +193,7 @@ public class Startup
|
||||
}
|
||||
|
||||
// Default Middleware
|
||||
app.UseDefaultMiddleware(env, globalSettings);
|
||||
app.UseDefaultMiddleware(environment, globalSettings);
|
||||
|
||||
if (!globalSettings.SelfHosted)
|
||||
{
|
||||
@@ -204,7 +201,7 @@ public class Startup
|
||||
app.UseMiddleware<CustomIpRateLimitMiddleware>();
|
||||
}
|
||||
|
||||
if (env.IsDevelopment())
|
||||
if (environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseDeveloperExceptionPage();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Data;
|
||||
using Bit.Core;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Entities;
|
||||
@@ -8,6 +9,7 @@ using Bit.Core.Tools.Repositories;
|
||||
using Bit.Infrastructure.Dapper.Repositories;
|
||||
using Bit.Infrastructure.Dapper.Tools.Helpers;
|
||||
using Dapper;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace Bit.Infrastructure.Dapper.Tools.Repositories;
|
||||
@@ -15,13 +17,24 @@ namespace Bit.Infrastructure.Dapper.Tools.Repositories;
|
||||
/// <inheritdoc cref="ISendRepository" />
|
||||
public class SendRepository : Repository<Send, Guid>, ISendRepository
|
||||
{
|
||||
public SendRepository(GlobalSettings globalSettings)
|
||||
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
|
||||
private readonly IDataProtector _dataProtector;
|
||||
|
||||
public SendRepository(GlobalSettings globalSettings, IDataProtectionProvider dataProtectionProvider)
|
||||
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString, dataProtectionProvider)
|
||||
{ }
|
||||
|
||||
public SendRepository(string connectionString, string readOnlyConnectionString)
|
||||
public SendRepository(string connectionString, string readOnlyConnectionString, IDataProtectionProvider dataProtectionProvider)
|
||||
: base(connectionString, readOnlyConnectionString)
|
||||
{ }
|
||||
{
|
||||
_dataProtector = dataProtectionProvider.CreateProtector(Constants.DatabaseFieldProtectorPurpose);
|
||||
}
|
||||
|
||||
public override async Task<Send?> GetByIdAsync(Guid id)
|
||||
{
|
||||
var send = await base.GetByIdAsync(id);
|
||||
UnprotectData(send);
|
||||
return send;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ICollection<Send>> GetManyByUserIdAsync(Guid userId)
|
||||
@@ -33,7 +46,9 @@ public class SendRepository : Repository<Send, Guid>, ISendRepository
|
||||
new { UserId = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
var sends = results.ToList();
|
||||
UnprotectData(sends);
|
||||
return sends;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,15 +62,35 @@ public class SendRepository : Repository<Send, Guid>, ISendRepository
|
||||
new { DeletionDate = deletionDateBefore },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
var sends = results.ToList();
|
||||
UnprotectData(sends);
|
||||
return sends;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<Send> CreateAsync(Send send)
|
||||
{
|
||||
await ProtectDataAndSaveAsync(send, async () => await base.CreateAsync(send));
|
||||
return send;
|
||||
}
|
||||
|
||||
public override async Task ReplaceAsync(Send send)
|
||||
{
|
||||
await ProtectDataAndSaveAsync(send, async () => await base.ReplaceAsync(send));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId, IEnumerable<Send> sends)
|
||||
{
|
||||
return async (connection, transaction) =>
|
||||
{
|
||||
// Protect all sends before bulk update
|
||||
var sendsList = sends.ToList();
|
||||
foreach (var send in sendsList)
|
||||
{
|
||||
ProtectData(send);
|
||||
}
|
||||
|
||||
// Create temp table
|
||||
var sqlCreateTemp = @"
|
||||
SELECT TOP 0 *
|
||||
@@ -71,7 +106,7 @@ public class SendRepository : Repository<Send, Guid>, ISendRepository
|
||||
using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction))
|
||||
{
|
||||
bulkCopy.DestinationTableName = "#TempSend";
|
||||
var sendsTable = sends.ToDataTable();
|
||||
var sendsTable = sendsList.ToDataTable();
|
||||
foreach (DataColumn col in sendsTable.Columns)
|
||||
{
|
||||
bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName);
|
||||
@@ -101,6 +136,69 @@ public class SendRepository : Repository<Send, Guid>, ISendRepository
|
||||
cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = userId;
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
// Unprotect after save
|
||||
foreach (var send in sendsList)
|
||||
{
|
||||
UnprotectData(send);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async Task ProtectDataAndSaveAsync(Send send, Func<Task> saveTask)
|
||||
{
|
||||
if (send == null)
|
||||
{
|
||||
await saveTask();
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture original value
|
||||
var originalEmailHashes = send.EmailHashes;
|
||||
|
||||
// Protect value
|
||||
ProtectData(send);
|
||||
|
||||
// Save
|
||||
await saveTask();
|
||||
|
||||
// Restore original value
|
||||
send.EmailHashes = originalEmailHashes;
|
||||
}
|
||||
|
||||
private void ProtectData(Send send)
|
||||
{
|
||||
if (!send.EmailHashes?.StartsWith(Constants.DatabaseFieldProtectedPrefix) ?? false)
|
||||
{
|
||||
send.EmailHashes = string.Concat(Constants.DatabaseFieldProtectedPrefix,
|
||||
_dataProtector.Protect(send.EmailHashes!));
|
||||
}
|
||||
}
|
||||
|
||||
private void UnprotectData(Send? send)
|
||||
{
|
||||
if (send == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (send.EmailHashes?.StartsWith(Constants.DatabaseFieldProtectedPrefix) ?? false)
|
||||
{
|
||||
send.EmailHashes = _dataProtector.Unprotect(
|
||||
send.EmailHashes.Substring(Constants.DatabaseFieldProtectedPrefix.Length));
|
||||
}
|
||||
}
|
||||
|
||||
private void UnprotectData(IEnumerable<Send> sends)
|
||||
{
|
||||
if (sends == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var send in sends)
|
||||
{
|
||||
UnprotectData(send);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ public class DatabaseContext : DbContext
|
||||
var eOrganizationDomain = builder.Entity<OrganizationDomain>();
|
||||
var aWebAuthnCredential = builder.Entity<WebAuthnCredential>();
|
||||
var eOrganizationMemberBaseDetail = builder.Entity<OrganizationMemberBaseDetail>();
|
||||
var eSend = builder.Entity<Send>();
|
||||
|
||||
// Shadow property configurations go here
|
||||
|
||||
@@ -148,6 +149,7 @@ public class DatabaseContext : DbContext
|
||||
var dataProtectionConverter = new DataProtectionConverter(dataProtector);
|
||||
eUser.Property(c => c.Key).HasConversion(dataProtectionConverter);
|
||||
eUser.Property(c => c.MasterPassword).HasConversion(dataProtectionConverter);
|
||||
eSend.Property(c => c.EmailHashes).HasConversion(dataProtectionConverter);
|
||||
|
||||
if (Database.IsNpgsql())
|
||||
{
|
||||
|
||||
@@ -5,7 +5,6 @@ using Bit.Core.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.IdentityModel.Logging;
|
||||
|
||||
namespace Bit.Notifications;
|
||||
|
||||
@@ -84,8 +83,6 @@ public class Startup
|
||||
IWebHostEnvironment env,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
IdentityModelEventSource.ShowPII = true;
|
||||
|
||||
// Add general security headers
|
||||
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||
|
||||
|
||||
@@ -472,11 +472,6 @@ public static class ServiceCollectionExtensions
|
||||
addAuthorization.Invoke(config);
|
||||
});
|
||||
}
|
||||
|
||||
if (environment.IsDevelopment())
|
||||
{
|
||||
Microsoft.IdentityModel.Logging.IdentityModelEventSource.ShowPII = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddCustomDataProtectionServices(
|
||||
@@ -666,7 +661,6 @@ public static class ServiceCollectionExtensions
|
||||
Constants.BrowserExtensions.OperaId
|
||||
};
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
-- FIXME: remove null default value once this argument has been
|
||||
-- in 2 server releases
|
||||
@Emails NVARCHAR(4000) = NULL,
|
||||
@AuthType TINYINT = NULL
|
||||
@AuthType TINYINT = NULL,
|
||||
@EmailHashes NVARCHAR(4000) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@@ -42,7 +43,8 @@ BEGIN
|
||||
[HideEmail],
|
||||
[CipherId],
|
||||
[Emails],
|
||||
[AuthType]
|
||||
[AuthType],
|
||||
[EmailHashes]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@@ -63,7 +65,8 @@ BEGIN
|
||||
@HideEmail,
|
||||
@CipherId,
|
||||
@Emails,
|
||||
@AuthType
|
||||
@AuthType,
|
||||
@EmailHashes
|
||||
)
|
||||
|
||||
IF @UserId IS NOT NULL
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
@HideEmail BIT,
|
||||
@CipherId UNIQUEIDENTIFIER = NULL,
|
||||
@Emails NVARCHAR(4000) = NULL,
|
||||
@AuthType TINYINT = NULL
|
||||
@AuthType TINYINT = NULL,
|
||||
@EmailHashes NVARCHAR(4000) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@@ -40,7 +41,8 @@ BEGIN
|
||||
[HideEmail] = @HideEmail,
|
||||
[CipherId] = @CipherId,
|
||||
[Emails] = @Emails,
|
||||
[AuthType] = @AuthType
|
||||
[AuthType] = @AuthType,
|
||||
[EmailHashes] = @EmailHashes
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
CREATE TABLE [dbo].[Send] (
|
||||
CREATE TABLE [dbo].[Send]
|
||||
(
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||
[UserId] UNIQUEIDENTIFIER NULL,
|
||||
[OrganizationId] UNIQUEIDENTIFIER NULL,
|
||||
[Type] TINYINT NOT NULL,
|
||||
[Data] VARCHAR(MAX) NOT NULL,
|
||||
[Key] VARCHAR (MAX) NOT NULL,
|
||||
[Password] NVARCHAR (300) NULL,
|
||||
[Emails] NVARCHAR (4000) NULL,
|
||||
[Key] VARCHAR(MAX) NOT NULL,
|
||||
[Password] NVARCHAR(300) NULL,
|
||||
[Emails] NVARCHAR(4000) NULL,
|
||||
[MaxAccessCount] INT NULL,
|
||||
[AccessCount] INT NOT NULL,
|
||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||
[ExpirationDate] DATETIME2 (7) NULL,
|
||||
[DeletionDate] DATETIME2 (7) NOT NULL,
|
||||
[CreationDate] DATETIME2(7) NOT NULL,
|
||||
[RevisionDate] DATETIME2(7) NOT NULL,
|
||||
[ExpirationDate] DATETIME2(7) NULL,
|
||||
[DeletionDate] DATETIME2(7) NOT NULL,
|
||||
[Disabled] BIT NOT NULL,
|
||||
[HideEmail] BIT NULL,
|
||||
[CipherId] UNIQUEIDENTIFIER NULL,
|
||||
[AuthType] TINYINT NULL,
|
||||
[EmailHashes] NVARCHAR(4000) NULL,
|
||||
CONSTRAINT [PK_Send] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||
CONSTRAINT [FK_Send_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]),
|
||||
CONSTRAINT [FK_Send_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]),
|
||||
@@ -26,9 +28,9 @@
|
||||
|
||||
GO
|
||||
CREATE NONCLUSTERED INDEX [IX_Send_UserId_OrganizationId]
|
||||
ON [dbo].[Send]([UserId] ASC, [OrganizationId] ASC);
|
||||
ON [dbo].[Send] ([UserId] ASC, [OrganizationId] ASC);
|
||||
|
||||
GO
|
||||
CREATE NONCLUSTERED INDEX [IX_Send_DeletionDate]
|
||||
ON [dbo].[Send]([DeletionDate] ASC);
|
||||
ON [dbo].[Send] ([DeletionDate] ASC);
|
||||
|
||||
|
||||
@@ -150,8 +150,8 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
|
||||
Enabled = true,
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "minComplexity", 10 },
|
||||
{ "minLength", 12 },
|
||||
{ "minComplexity", 4 },
|
||||
{ "minLength", 128 },
|
||||
{ "requireUpper", true },
|
||||
{ "requireLower", false },
|
||||
{ "requireNumbers", true },
|
||||
@@ -397,4 +397,48 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_MasterPasswordPolicy_ExcessiveMinLength_ReturnsBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var policyType = PolicyType.MasterPassword;
|
||||
var request = new PolicyRequestModel
|
||||
{
|
||||
Enabled = true,
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "minLength", 129 }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}",
|
||||
JsonContent.Create(request));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_MasterPasswordPolicy_ExcessiveMinComplexity_ReturnsBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var policyType = PolicyType.MasterPassword;
|
||||
var request = new PolicyRequestModel
|
||||
{
|
||||
Enabled = true,
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "minComplexity", 5 }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsync($"/organizations/{_organization.Id}/policies/{policyType}",
|
||||
JsonContent.Create(request));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,4 +264,138 @@ public class MembersControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
|
||||
new Permissions { CreateNewCollections = true, ManageScim = true, ManageGroups = true, ManageUsers = true },
|
||||
orgUser.GetPermissions());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Revoke_Member_Success()
|
||||
{
|
||||
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
|
||||
_factory, _organization.Id, OrganizationUserType.User);
|
||||
|
||||
var response = await _client.PostAsync($"/public/members/{orgUser.Id}/revoke", null);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
var updatedUser = await _factory.GetService<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(orgUser.Id);
|
||||
Assert.NotNull(updatedUser);
|
||||
Assert.Equal(OrganizationUserStatusType.Revoked, updatedUser.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Revoke_AlreadyRevoked_ReturnsBadRequest()
|
||||
{
|
||||
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
|
||||
_factory, _organization.Id, OrganizationUserType.User);
|
||||
|
||||
var revokeResponse = await _client.PostAsync($"/public/members/{orgUser.Id}/revoke", null);
|
||||
Assert.Equal(HttpStatusCode.OK, revokeResponse.StatusCode);
|
||||
|
||||
var response = await _client.PostAsync($"/public/members/{orgUser.Id}/revoke", null);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
var error = await response.Content.ReadFromJsonAsync<ErrorResponseModel>();
|
||||
Assert.Equal("Already revoked.", error?.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Revoke_NotFound_ReturnsNotFound()
|
||||
{
|
||||
var response = await _client.PostAsync($"/public/members/{Guid.NewGuid()}/revoke", null);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Revoke_DifferentOrganization_ReturnsNotFound()
|
||||
{
|
||||
// Create a different organization
|
||||
var ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||
await _factory.LoginWithNewAccount(ownerEmail);
|
||||
var (otherOrganization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
|
||||
ownerEmail: ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
|
||||
|
||||
// Create a user in the other organization
|
||||
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
|
||||
_factory, otherOrganization.Id, OrganizationUserType.User);
|
||||
|
||||
// Re-authenticate with the original organization
|
||||
await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id);
|
||||
|
||||
// Try to revoke the user from the other organization
|
||||
var response = await _client.PostAsync($"/public/members/{orgUser.Id}/revoke", null);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Restore_Member_Success()
|
||||
{
|
||||
// Invite a user to revoke
|
||||
var email = $"integration-test{Guid.NewGuid()}@example.com";
|
||||
var inviteRequest = new MemberCreateRequestModel
|
||||
{
|
||||
Email = email,
|
||||
Type = OrganizationUserType.User,
|
||||
};
|
||||
|
||||
var inviteResponse = await _client.PostAsync("/public/members", JsonContent.Create(inviteRequest));
|
||||
Assert.Equal(HttpStatusCode.OK, inviteResponse.StatusCode);
|
||||
var invitedMember = await inviteResponse.Content.ReadFromJsonAsync<MemberResponseModel>();
|
||||
Assert.NotNull(invitedMember);
|
||||
|
||||
// Revoke the invited user
|
||||
var revokeResponse = await _client.PostAsync($"/public/members/{invitedMember.Id}/revoke", null);
|
||||
Assert.Equal(HttpStatusCode.OK, revokeResponse.StatusCode);
|
||||
|
||||
// Restore the user
|
||||
var response = await _client.PostAsync($"/public/members/{invitedMember.Id}/restore", null);
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
// Verify user is restored to Invited state
|
||||
var updatedUser = await _factory.GetService<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(invitedMember.Id);
|
||||
Assert.NotNull(updatedUser);
|
||||
Assert.Equal(OrganizationUserStatusType.Invited, updatedUser.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Restore_AlreadyActive_ReturnsBadRequest()
|
||||
{
|
||||
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
|
||||
_factory, _organization.Id, OrganizationUserType.User);
|
||||
|
||||
var response = await _client.PostAsync($"/public/members/{orgUser.Id}/restore", null);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
var error = await response.Content.ReadFromJsonAsync<ErrorResponseModel>();
|
||||
Assert.Equal("Already active.", error?.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Restore_NotFound_ReturnsNotFound()
|
||||
{
|
||||
var response = await _client.PostAsync($"/public/members/{Guid.NewGuid()}/restore", null);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Restore_DifferentOrganization_ReturnsNotFound()
|
||||
{
|
||||
// Create a different organization
|
||||
var ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||
await _factory.LoginWithNewAccount(ownerEmail);
|
||||
var (otherOrganization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
|
||||
ownerEmail: ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
|
||||
|
||||
// Create a user in the other organization
|
||||
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
|
||||
_factory, otherOrganization.Id, OrganizationUserType.User);
|
||||
|
||||
// Re-authenticate with the original organization
|
||||
await _loginHelper.LoginWithOrganizationApiKeyAsync(_organization.Id);
|
||||
|
||||
// Try to restore the user from the other organization
|
||||
var response = await _client.PostAsync($"/public/members/{orgUser.Id}/restore", null);
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,8 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
|
||||
Enabled = true,
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "minComplexity", 15},
|
||||
{ "minComplexity", 4},
|
||||
{ "minLength", 128 },
|
||||
{ "requireLower", true}
|
||||
}
|
||||
};
|
||||
@@ -78,7 +79,8 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
|
||||
Assert.IsType<Guid>(result.Id);
|
||||
Assert.NotEqual(default, result.Id);
|
||||
Assert.NotNull(result.Data);
|
||||
Assert.Equal(15, ((JsonElement)result.Data["minComplexity"]).GetInt32());
|
||||
Assert.Equal(4, ((JsonElement)result.Data["minComplexity"]).GetInt32());
|
||||
Assert.Equal(128, ((JsonElement)result.Data["minLength"]).GetInt32());
|
||||
Assert.True(((JsonElement)result.Data["requireLower"]).GetBoolean());
|
||||
|
||||
// Assert against the database values
|
||||
@@ -94,7 +96,7 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
|
||||
|
||||
Assert.NotNull(policy.Data);
|
||||
var data = policy.GetDataModel<MasterPasswordPolicyData>();
|
||||
var expectedData = new MasterPasswordPolicyData { MinComplexity = 15, RequireLower = true };
|
||||
var expectedData = new MasterPasswordPolicyData { MinComplexity = 4, MinLength = 128, RequireLower = true };
|
||||
AssertHelper.AssertPropertyEqual(expectedData, data);
|
||||
}
|
||||
|
||||
@@ -242,4 +244,46 @@ public class PoliciesControllerTests : IClassFixture<ApiApplicationFactory>, IAs
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_MasterPasswordPolicy_ExcessiveMinLength_ReturnsBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var policyType = PolicyType.MasterPassword;
|
||||
var request = new PolicyUpdateRequestModel
|
||||
{
|
||||
Enabled = true,
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "minLength", 129 }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsync($"/public/policies/{policyType}", JsonContent.Create(request));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Put_MasterPasswordPolicy_ExcessiveMinComplexity_ReturnsBadRequest()
|
||||
{
|
||||
// Arrange
|
||||
var policyType = PolicyType.MasterPassword;
|
||||
var request = new PolicyUpdateRequestModel
|
||||
{
|
||||
Enabled = true,
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "minComplexity", 5 }
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var response = await _client.PutAsync($"/public/policies/{policyType}", JsonContent.Create(request));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using Bit.Api.Models.Public.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Platform.Push;
|
||||
@@ -114,4 +115,64 @@ public class CollectionsControllerTests : IClassFixture<ApiApplicationFactory>,
|
||||
Assert.NotEmpty(result.Item2.Groups);
|
||||
Assert.NotEmpty(result.Item2.Users);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_ExcludesDefaultUserCollections_IncludesGroupsAndUsers()
|
||||
{
|
||||
// Arrange
|
||||
var collectionRepository = _factory.GetService<ICollectionRepository>();
|
||||
var groupRepository = _factory.GetService<IGroupRepository>();
|
||||
|
||||
var defaultCollection = new Collection
|
||||
{
|
||||
OrganizationId = _organization.Id,
|
||||
Name = "My Items",
|
||||
Type = CollectionType.DefaultUserCollection
|
||||
};
|
||||
await collectionRepository.CreateAsync(defaultCollection, null, null);
|
||||
|
||||
var group = await groupRepository.CreateAsync(new Group
|
||||
{
|
||||
OrganizationId = _organization.Id,
|
||||
Name = "Test Group",
|
||||
ExternalId = $"test-group-{Guid.NewGuid()}",
|
||||
});
|
||||
|
||||
var (_, user) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
|
||||
_factory,
|
||||
_organization.Id,
|
||||
OrganizationUserType.User);
|
||||
|
||||
var sharedCollection = await OrganizationTestHelpers.CreateCollectionAsync(
|
||||
_factory,
|
||||
_organization.Id,
|
||||
"Shared Collection with Access",
|
||||
externalId: "shared-collection-with-access",
|
||||
groups:
|
||||
[
|
||||
new CollectionAccessSelection { Id = group.Id, ReadOnly = false, HidePasswords = false, Manage = true }
|
||||
],
|
||||
users:
|
||||
[
|
||||
new CollectionAccessSelection { Id = user.Id, ReadOnly = true, HidePasswords = true, Manage = false }
|
||||
]);
|
||||
|
||||
// Act
|
||||
var response = await _client.GetFromJsonAsync<ListResponseModel<CollectionResponseModel>>("public/collections");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
|
||||
Assert.DoesNotContain(response.Data, c => c.Id == defaultCollection.Id);
|
||||
|
||||
var collectionResponse = response.Data.First(c => c.Id == sharedCollection.Id);
|
||||
Assert.NotNull(collectionResponse.Groups);
|
||||
Assert.Single(collectionResponse.Groups);
|
||||
|
||||
var groupResponse = collectionResponse.Groups.First();
|
||||
Assert.Equal(group.Id, groupResponse.Id);
|
||||
Assert.False(groupResponse.ReadOnly);
|
||||
Assert.False(groupResponse.HidePasswords);
|
||||
Assert.True(groupResponse.Manage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Results;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
@@ -30,6 +29,7 @@ using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -137,23 +137,20 @@ public class OrganizationUsersControllerTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Accept_WhenOrganizationUsePoliciesIsEnabledAndResetPolicyIsEnabled_ShouldHandleResetPassword(Guid orgId, Guid orgUserId,
|
||||
OrganizationUserAcceptRequestModel model, User user, SutProvider<OrganizationUsersController> sutProvider)
|
||||
OrganizationUserAcceptRequestModel model, User user,
|
||||
[Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
|
||||
applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = true });
|
||||
|
||||
var policy = new Policy
|
||||
{
|
||||
Enabled = true,
|
||||
Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }),
|
||||
};
|
||||
policy.Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, });
|
||||
var userService = sutProvider.GetDependency<IUserService>();
|
||||
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||
|
||||
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
policyRepository.GetByOrganizationIdTypeAsync(orgId,
|
||||
var policyQuery = sutProvider.GetDependency<IPolicyQuery>();
|
||||
policyQuery.RunAsync(orgId,
|
||||
PolicyType.ResetPassword).Returns(policy);
|
||||
|
||||
// Act
|
||||
@@ -167,29 +164,27 @@ public class OrganizationUsersControllerTests
|
||||
|
||||
await userService.Received(1).GetUserByPrincipalAsync(default);
|
||||
await applicationCacheService.Received(1).GetOrganizationAbilityAsync(orgId);
|
||||
await policyRepository.Received(1).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
||||
await policyQuery.Received(1).RunAsync(orgId, PolicyType.ResetPassword);
|
||||
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Accept_WhenOrganizationUsePoliciesIsDisabled_ShouldNotHandleResetPassword(Guid orgId, Guid orgUserId,
|
||||
OrganizationUserAcceptRequestModel model, User user, SutProvider<OrganizationUsersController> sutProvider)
|
||||
OrganizationUserAcceptRequestModel model, User user,
|
||||
[Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
|
||||
applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = false });
|
||||
|
||||
var policy = new Policy
|
||||
{
|
||||
Enabled = true,
|
||||
Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }),
|
||||
};
|
||||
policy.Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, });
|
||||
var userService = sutProvider.GetDependency<IUserService>();
|
||||
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
policyRepository.GetByOrganizationIdTypeAsync(orgId,
|
||||
var policyQuery = sutProvider.GetDependency<IPolicyQuery>();
|
||||
policyQuery.RunAsync(orgId,
|
||||
PolicyType.ResetPassword).Returns(policy);
|
||||
|
||||
// Act
|
||||
@@ -202,7 +197,7 @@ public class OrganizationUsersControllerTests
|
||||
await sutProvider.GetDependency<IOrganizationService>().Received(0)
|
||||
.UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);
|
||||
|
||||
await policyRepository.Received(0).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
||||
await policyQuery.Received(0).RunAsync(orgId, PolicyType.ResetPassword);
|
||||
await applicationCacheService.Received(1).GetOrganizationAbilityAsync(orgId);
|
||||
}
|
||||
|
||||
@@ -383,7 +378,7 @@ public class OrganizationUsersControllerTests
|
||||
|
||||
var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
|
||||
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
var policyQuery = sutProvider.GetDependency<IPolicyQuery>();
|
||||
|
||||
var policyRequirement = new ResetPasswordPolicyRequirement { AutoEnrollOrganizations = [orgId] };
|
||||
|
||||
@@ -400,7 +395,7 @@ public class OrganizationUsersControllerTests
|
||||
|
||||
await userService.Received(1).GetUserByPrincipalAsync(default);
|
||||
await applicationCacheService.Received(0).GetOrganizationAbilityAsync(orgId);
|
||||
await policyRepository.Received(0).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
||||
await policyQuery.Received(0).RunAsync(orgId, PolicyType.ResetPassword);
|
||||
await policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||
Assert.True(policyRequirement.AutoEnrollEnabled(orgId));
|
||||
}
|
||||
@@ -425,7 +420,7 @@ public class OrganizationUsersControllerTests
|
||||
var userService = sutProvider.GetDependency<IUserService>();
|
||||
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
var policyQuery = sutProvider.GetDependency<IPolicyQuery>();
|
||||
|
||||
var policyRequirementQuery = sutProvider.GetDependency<IPolicyRequirementQuery>();
|
||||
|
||||
@@ -445,7 +440,7 @@ public class OrganizationUsersControllerTests
|
||||
|
||||
await userService.Received(1).GetUserByPrincipalAsync(default);
|
||||
await applicationCacheService.Received(0).GetOrganizationAbilityAsync(orgId);
|
||||
await policyRepository.Received(0).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
||||
await policyQuery.Received(0).RunAsync(orgId, PolicyType.ResetPassword);
|
||||
await policyRequirementQuery.Received(1).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||
|
||||
Assert.Equal("Master Password reset is required, but not provided.", exception.Message);
|
||||
@@ -734,7 +729,7 @@ public class OrganizationUsersControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task BulkReinvite_WhenFeatureFlagEnabled_UsesBulkResendOrganizationInvitesCommand(
|
||||
public async Task BulkReinvite_UsesBulkResendOrganizationInvitesCommand(
|
||||
Guid organizationId,
|
||||
OrganizationUserBulkRequestModel bulkRequestModel,
|
||||
List<OrganizationUser> organizationUsers,
|
||||
@@ -744,9 +739,6 @@ public class OrganizationUsersControllerTests
|
||||
// Arrange
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.IncreaseBulkReinviteLimitForCloud)
|
||||
.Returns(true);
|
||||
|
||||
var expectedResults = organizationUsers.Select(u => Tuple.Create(u, "")).ToList();
|
||||
sutProvider.GetDependency<IBulkResendOrganizationInvitesCommand>()
|
||||
@@ -763,36 +755,4 @@ public class OrganizationUsersControllerTests
|
||||
.Received(1)
|
||||
.BulkResendInvitesAsync(organizationId, userId, bulkRequestModel.Ids);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task BulkReinvite_WhenFeatureFlagDisabled_UsesLegacyOrganizationService(
|
||||
Guid organizationId,
|
||||
OrganizationUserBulkRequestModel bulkRequestModel,
|
||||
List<OrganizationUser> organizationUsers,
|
||||
Guid userId,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(Arg.Any<ClaimsPrincipal>()).Returns(userId);
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.IncreaseBulkReinviteLimitForCloud)
|
||||
.Returns(false);
|
||||
|
||||
var expectedResults = organizationUsers.Select(u => Tuple.Create(u, "")).ToList();
|
||||
sutProvider.GetDependency<IOrganizationService>()
|
||||
.ResendInvitesAsync(organizationId, userId, bulkRequestModel.Ids)
|
||||
.Returns(expectedResults);
|
||||
|
||||
// Act
|
||||
var response = await sutProvider.Sut.BulkReinvite(organizationId, bulkRequestModel);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(organizationUsers.Count, response.Data.Count());
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationService>()
|
||||
.Received(1)
|
||||
.ResendInvitesAsync(organizationId, userId, bulkRequestModel.Ids);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
@@ -25,6 +26,7 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
@@ -200,28 +202,21 @@ public class OrganizationsControllerTests
|
||||
SutProvider<OrganizationsController> sutProvider,
|
||||
User user,
|
||||
Organization organization,
|
||||
OrganizationUser organizationUser)
|
||||
OrganizationUser organizationUser,
|
||||
[Policy(PolicyType.ResetPassword, data: "{\"AutoEnrollEnabled\": true}")] PolicyStatus policy)
|
||||
{
|
||||
var policy = new Policy
|
||||
{
|
||||
Type = PolicyType.ResetPassword,
|
||||
Enabled = true,
|
||||
Data = "{\"AutoEnrollEnabled\": true}",
|
||||
OrganizationId = organization.Id
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(organization.Id.ToString()).Returns(organization);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(false);
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(organization.Id, user.Id).Returns(organizationUser);
|
||||
sutProvider.GetDependency<IPolicyRepository>().GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword).Returns(policy);
|
||||
sutProvider.GetDependency<IPolicyQuery>().RunAsync(organization.Id, PolicyType.ResetPassword).Returns(policy);
|
||||
|
||||
var result = await sutProvider.Sut.GetAutoEnrollStatus(organization.Id.ToString());
|
||||
|
||||
await sutProvider.GetDependency<IUserService>().Received(1).GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>());
|
||||
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetByIdentifierAsync(organization.Id.ToString());
|
||||
await sutProvider.GetDependency<IPolicyRequirementQuery>().Received(0).GetAsync<ResetPasswordPolicyRequirement>(user.Id);
|
||||
await sutProvider.GetDependency<IPolicyRepository>().Received(1).GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword);
|
||||
await sutProvider.GetDependency<IPolicyQuery>().Received(1).RunAsync(organization.Id, PolicyType.ResetPassword);
|
||||
|
||||
Assert.True(result.ResetPasswordEnabled);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
using AutoFixture;
|
||||
using Bit.Api.AdminConsole.Models.Response.Helpers;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Api.AdminConsole.Models.Response.Helpers;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Models.Response.Helpers;
|
||||
|
||||
public class PolicyDetailResponsesTests
|
||||
public class PolicyStatusResponsesTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(true, false)]
|
||||
@@ -17,19 +16,13 @@ public class PolicyDetailResponsesTests
|
||||
bool policyEnabled,
|
||||
bool expectedCanToggle)
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
|
||||
var policy = fixture.Build<Policy>()
|
||||
.Without(p => p.Data)
|
||||
.With(p => p.Type, PolicyType.SingleOrg)
|
||||
.With(p => p.Enabled, policyEnabled)
|
||||
.Create();
|
||||
var policy = new PolicyStatus(Guid.NewGuid(), PolicyType.SingleOrg) { Enabled = policyEnabled };
|
||||
|
||||
var querySub = Substitute.For<IOrganizationHasVerifiedDomainsQuery>();
|
||||
querySub.HasVerifiedDomainsAsync(policy.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
var result = await policy.GetSingleOrgPolicyDetailResponseAsync(querySub);
|
||||
var result = await policy.GetSingleOrgPolicyStatusResponseAsync(querySub);
|
||||
|
||||
Assert.Equal(expectedCanToggle, result.CanToggleState);
|
||||
}
|
||||
@@ -37,18 +30,13 @@ public class PolicyDetailResponsesTests
|
||||
[Fact]
|
||||
public async Task GetSingleOrgPolicyDetailResponseAsync_WhenIsNotSingleOrgType_ThenShouldThrowArgumentException()
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
|
||||
var policy = fixture.Build<Policy>()
|
||||
.Without(p => p.Data)
|
||||
.With(p => p.Type, PolicyType.TwoFactorAuthentication)
|
||||
.Create();
|
||||
var policy = new PolicyStatus(Guid.NewGuid(), PolicyType.TwoFactorAuthentication);
|
||||
|
||||
var querySub = Substitute.For<IOrganizationHasVerifiedDomainsQuery>();
|
||||
querySub.HasVerifiedDomainsAsync(policy.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
var action = async () => await policy.GetSingleOrgPolicyDetailResponseAsync(querySub);
|
||||
var action = async () => await policy.GetSingleOrgPolicyStatusResponseAsync(querySub);
|
||||
|
||||
await Assert.ThrowsAsync<ArgumentException>("policy", action);
|
||||
}
|
||||
@@ -56,18 +44,13 @@ public class PolicyDetailResponsesTests
|
||||
[Fact]
|
||||
public async Task GetSingleOrgPolicyDetailResponseAsync_WhenIsSingleOrgTypeAndDoesNotHaveVerifiedDomains_ThenShouldBeAbleToToggle()
|
||||
{
|
||||
var fixture = new Fixture();
|
||||
|
||||
var policy = fixture.Build<Policy>()
|
||||
.Without(p => p.Data)
|
||||
.With(p => p.Type, PolicyType.SingleOrg)
|
||||
.Create();
|
||||
var policy = new PolicyStatus(Guid.NewGuid(), PolicyType.SingleOrg);
|
||||
|
||||
var querySub = Substitute.For<IOrganizationHasVerifiedDomainsQuery>();
|
||||
querySub.HasVerifiedDomainsAsync(policy.OrganizationId)
|
||||
.Returns(false);
|
||||
|
||||
var result = await policy.GetSingleOrgPolicyDetailResponseAsync(querySub);
|
||||
var result = await policy.GetSingleOrgPolicyStatusResponseAsync(querySub);
|
||||
|
||||
Assert.True(result.CanToggleState);
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
using Bit.Api.Billing.Controllers;
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@@ -10,6 +13,7 @@ using Bit.Core.Models.Data;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core.Test.Billing.Mocks;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -82,7 +86,9 @@ public class OrganizationSponsorshipsControllerTests
|
||||
[BitAutoData]
|
||||
public async Task RedeemSponsorship_NotSponsoredOrgOwner_Success(string sponsorshipToken, User user,
|
||||
OrganizationSponsorship sponsorship, Organization sponsoringOrganization,
|
||||
OrganizationSponsorshipRedeemRequestModel model, SutProvider<OrganizationSponsorshipsController> sutProvider)
|
||||
OrganizationSponsorshipRedeemRequestModel model,
|
||||
[Policy(PolicyType.FreeFamiliesSponsorshipPolicy, false)] PolicyStatus policy,
|
||||
SutProvider<OrganizationSponsorshipsController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(user.Id);
|
||||
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(user.Id)
|
||||
@@ -91,6 +97,9 @@ public class OrganizationSponsorshipsControllerTests
|
||||
user.Email).Returns((true, sponsorship));
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(model.SponsoredOrganizationId).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(model.SponsoredOrganizationId).Returns(sponsoringOrganization);
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(Arg.Any<Guid>(), PolicyType.FreeFamiliesSponsorshipPolicy)
|
||||
.Returns(policy);
|
||||
|
||||
await sutProvider.Sut.RedeemSponsorship(sponsorshipToken, model);
|
||||
|
||||
@@ -101,14 +110,18 @@ public class OrganizationSponsorshipsControllerTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreValidateSponsorshipToken_ValidatesToken_Success(string sponsorshipToken, User user,
|
||||
OrganizationSponsorship sponsorship, SutProvider<OrganizationSponsorshipsController> sutProvider)
|
||||
OrganizationSponsorship sponsorship,
|
||||
[Policy(PolicyType.FreeFamiliesSponsorshipPolicy, false)] PolicyStatus policy,
|
||||
SutProvider<OrganizationSponsorshipsController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(user.Id);
|
||||
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
sutProvider.GetDependency<IValidateRedemptionTokenCommand>()
|
||||
.ValidateRedemptionTokenAsync(sponsorshipToken, user.Email).Returns((true, sponsorship));
|
||||
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(Arg.Any<Guid>(), PolicyType.FreeFamiliesSponsorshipPolicy)
|
||||
.Returns(policy);
|
||||
await sutProvider.Sut.PreValidateSponsorshipToken(sponsorshipToken);
|
||||
|
||||
await sutProvider.GetDependency<IValidateRedemptionTokenCommand>().Received(1)
|
||||
|
||||
@@ -49,7 +49,7 @@ public class PoliciesControllerTests
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns((Guid?)userId);
|
||||
.Returns(userId);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByOrganizationAsync(orgId, userId)
|
||||
@@ -95,7 +95,7 @@ public class PoliciesControllerTests
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns((Guid?)userId);
|
||||
.Returns(userId);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByOrganizationAsync(orgId, userId)
|
||||
@@ -113,7 +113,7 @@ public class PoliciesControllerTests
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns((Guid?)userId);
|
||||
.Returns(userId);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByOrganizationAsync(orgId, userId)
|
||||
@@ -135,7 +135,7 @@ public class PoliciesControllerTests
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns((Guid?)userId);
|
||||
.Returns(userId);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByOrganizationAsync(orgId, userId)
|
||||
@@ -186,59 +186,35 @@ public class PoliciesControllerTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Get_WhenUserCanManagePolicies_WithExistingType_ReturnsExistingPolicy(
|
||||
SutProvider<PoliciesController> sutProvider, Guid orgId, Policy policy, int type)
|
||||
SutProvider<PoliciesController> sutProvider, Guid orgId, PolicyStatus policy, PolicyType type)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ManagePolicies(orgId)
|
||||
.Returns(true);
|
||||
|
||||
policy.Type = (PolicyType)type;
|
||||
policy.Type = type;
|
||||
policy.Enabled = true;
|
||||
policy.Data = null;
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type)
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(orgId, type)
|
||||
.Returns(policy);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Get(orgId, type);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<PolicyDetailResponseModel>(result);
|
||||
Assert.Equal(policy.Id, result.Id);
|
||||
Assert.IsType<PolicyStatusResponseModel>(result);
|
||||
Assert.Equal(policy.Type, result.Type);
|
||||
Assert.Equal(policy.Enabled, result.Enabled);
|
||||
Assert.Equal(policy.OrganizationId, result.OrganizationId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Get_WhenUserCanManagePolicies_WithNonExistingType_ReturnsDefaultPolicy(
|
||||
SutProvider<PoliciesController> sutProvider, Guid orgId, int type)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ManagePolicies(orgId)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type)
|
||||
.Returns((Policy)null);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Get(orgId, type);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<PolicyDetailResponseModel>(result);
|
||||
Assert.Equal(result.Type, (PolicyType)type);
|
||||
Assert.False(result.Enabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Get_WhenUserCannotManagePolicies_ThrowsNotFoundException(
|
||||
SutProvider<PoliciesController> sutProvider, Guid orgId, int type)
|
||||
SutProvider<PoliciesController> sutProvider, Guid orgId, PolicyType type)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
|
||||
@@ -69,6 +69,44 @@ public class OrganizationUserRotationValidatorTests
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData([null])]
|
||||
[BitAutoData("")]
|
||||
public async Task ValidateAsync_OrgUsersWithNullOrEmptyResetPasswordKey_FiltersOutInvalidKeys(
|
||||
string? invalidResetPasswordKey,
|
||||
SutProvider<OrganizationUserRotationValidator> sutProvider, User user,
|
||||
ResetPasswordWithOrgIdRequestModel validResetPasswordKey)
|
||||
{
|
||||
// Arrange
|
||||
var existingUserResetPassword = new List<OrganizationUser>
|
||||
{
|
||||
// Valid org user with reset password key
|
||||
new OrganizationUser
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = validResetPasswordKey.OrganizationId,
|
||||
ResetPasswordKey = validResetPasswordKey.ResetPasswordKey
|
||||
},
|
||||
// Invalid org user with null or empty reset password key - should be filtered out
|
||||
new OrganizationUser
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
ResetPasswordKey = invalidResetPasswordKey
|
||||
}
|
||||
};
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id)
|
||||
.Returns(existingUserResetPassword);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(user, new[] { validResetPasswordKey });
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Single(result);
|
||||
Assert.Equal(validResetPasswordKey.OrganizationId, result[0].OrganizationId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_MissingResetPassword_Throws(
|
||||
|
||||
@@ -981,205 +981,6 @@ public class SendsControllerTests : IDisposable
|
||||
Assert.Equal(expectedUrl, response.Url);
|
||||
}
|
||||
|
||||
#region AccessUsingAuth Validation Tests
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task AccessUsingAuth_WithExpiredSend_ThrowsNotFoundException(Guid sendId)
|
||||
{
|
||||
var send = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
UserId = Guid.NewGuid(),
|
||||
Type = SendType.Text,
|
||||
Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)),
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7),
|
||||
ExpirationDate = DateTime.UtcNow.AddDays(-1), // Expired yesterday
|
||||
Disabled = false,
|
||||
AccessCount = 0,
|
||||
MaxAccessCount = null
|
||||
};
|
||||
var user = CreateUserWithSendIdClaim(sendId);
|
||||
_sut.ControllerContext = CreateControllerContextWithUser(user);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.AccessUsingAuth());
|
||||
|
||||
await _sendRepository.Received(1).GetByIdAsync(sendId);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task AccessUsingAuth_WithDeletedSend_ThrowsNotFoundException(Guid sendId)
|
||||
{
|
||||
var send = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
UserId = Guid.NewGuid(),
|
||||
Type = SendType.Text,
|
||||
Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)),
|
||||
DeletionDate = DateTime.UtcNow.AddDays(-1), // Should have been deleted yesterday
|
||||
ExpirationDate = null,
|
||||
Disabled = false,
|
||||
AccessCount = 0,
|
||||
MaxAccessCount = null
|
||||
};
|
||||
var user = CreateUserWithSendIdClaim(sendId);
|
||||
_sut.ControllerContext = CreateControllerContextWithUser(user);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.AccessUsingAuth());
|
||||
|
||||
await _sendRepository.Received(1).GetByIdAsync(sendId);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task AccessUsingAuth_WithDisabledSend_ThrowsNotFoundException(Guid sendId)
|
||||
{
|
||||
var send = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
UserId = Guid.NewGuid(),
|
||||
Type = SendType.Text,
|
||||
Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)),
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7),
|
||||
ExpirationDate = null,
|
||||
Disabled = true, // Disabled
|
||||
AccessCount = 0,
|
||||
MaxAccessCount = null
|
||||
};
|
||||
var user = CreateUserWithSendIdClaim(sendId);
|
||||
_sut.ControllerContext = CreateControllerContextWithUser(user);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.AccessUsingAuth());
|
||||
|
||||
await _sendRepository.Received(1).GetByIdAsync(sendId);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task AccessUsingAuth_WithAccessCountExceeded_ThrowsNotFoundException(Guid sendId)
|
||||
{
|
||||
var send = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
UserId = Guid.NewGuid(),
|
||||
Type = SendType.Text,
|
||||
Data = JsonSerializer.Serialize(new SendTextData("Test", "Notes", "Text", false)),
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7),
|
||||
ExpirationDate = null,
|
||||
Disabled = false,
|
||||
AccessCount = 5,
|
||||
MaxAccessCount = 5 // Limit reached
|
||||
};
|
||||
var user = CreateUserWithSendIdClaim(sendId);
|
||||
_sut.ControllerContext = CreateControllerContextWithUser(user);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.AccessUsingAuth());
|
||||
|
||||
await _sendRepository.Received(1).GetByIdAsync(sendId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetSendFileDownloadDataUsingAuth Validation Tests
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task GetSendFileDownloadDataUsingAuth_WithExpiredSend_ThrowsNotFoundException(
|
||||
Guid sendId, string fileId)
|
||||
{
|
||||
var send = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
Type = SendType.File,
|
||||
Data = JsonSerializer.Serialize(new SendFileData("Test", "Notes", "file.pdf")),
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7),
|
||||
ExpirationDate = DateTime.UtcNow.AddDays(-1), // Expired
|
||||
Disabled = false,
|
||||
AccessCount = 0,
|
||||
MaxAccessCount = null
|
||||
};
|
||||
var user = CreateUserWithSendIdClaim(sendId);
|
||||
_sut.ControllerContext = CreateControllerContextWithUser(user);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.GetSendFileDownloadDataUsingAuth(fileId));
|
||||
|
||||
await _sendRepository.Received(1).GetByIdAsync(sendId);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task GetSendFileDownloadDataUsingAuth_WithDeletedSend_ThrowsNotFoundException(
|
||||
Guid sendId, string fileId)
|
||||
{
|
||||
var send = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
Type = SendType.File,
|
||||
Data = JsonSerializer.Serialize(new SendFileData("Test", "Notes", "file.pdf")),
|
||||
DeletionDate = DateTime.UtcNow.AddDays(-1), // Deleted
|
||||
ExpirationDate = null,
|
||||
Disabled = false,
|
||||
AccessCount = 0,
|
||||
MaxAccessCount = null
|
||||
};
|
||||
var user = CreateUserWithSendIdClaim(sendId);
|
||||
_sut.ControllerContext = CreateControllerContextWithUser(user);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.GetSendFileDownloadDataUsingAuth(fileId));
|
||||
|
||||
await _sendRepository.Received(1).GetByIdAsync(sendId);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task GetSendFileDownloadDataUsingAuth_WithDisabledSend_ThrowsNotFoundException(
|
||||
Guid sendId, string fileId)
|
||||
{
|
||||
var send = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
Type = SendType.File,
|
||||
Data = JsonSerializer.Serialize(new SendFileData("Test", "Notes", "file.pdf")),
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7),
|
||||
ExpirationDate = null,
|
||||
Disabled = true, // Disabled
|
||||
AccessCount = 0,
|
||||
MaxAccessCount = null
|
||||
};
|
||||
var user = CreateUserWithSendIdClaim(sendId);
|
||||
_sut.ControllerContext = CreateControllerContextWithUser(user);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.GetSendFileDownloadDataUsingAuth(fileId));
|
||||
|
||||
await _sendRepository.Received(1).GetByIdAsync(sendId);
|
||||
}
|
||||
|
||||
[Theory, AutoData]
|
||||
public async Task GetSendFileDownloadDataUsingAuth_WithAccessCountExceeded_ThrowsNotFoundException(
|
||||
Guid sendId, string fileId)
|
||||
{
|
||||
var send = new Send
|
||||
{
|
||||
Id = sendId,
|
||||
Type = SendType.File,
|
||||
Data = JsonSerializer.Serialize(new SendFileData("Test", "Notes", "file.pdf")),
|
||||
DeletionDate = DateTime.UtcNow.AddDays(7),
|
||||
ExpirationDate = null,
|
||||
Disabled = false,
|
||||
AccessCount = 10,
|
||||
MaxAccessCount = 10 // Limit reached
|
||||
};
|
||||
var user = CreateUserWithSendIdClaim(sendId);
|
||||
_sut.ControllerContext = CreateControllerContextWithUser(user);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => _sut.GetSendFileDownloadDataUsingAuth(fileId));
|
||||
|
||||
await _sendRepository.Received(1).GetByIdAsync(sendId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
@@ -28,7 +29,13 @@ public class StripeEventServiceTests
|
||||
_providerRepository = Substitute.For<IProviderRepository>();
|
||||
_setupIntentCache = Substitute.For<ISetupIntentCache>();
|
||||
_stripeFacade = Substitute.For<IStripeFacade>();
|
||||
_stripeEventService = new StripeEventService(globalSettings, _organizationRepository, _providerRepository, _setupIntentCache, _stripeFacade);
|
||||
_stripeEventService = new StripeEventService(
|
||||
globalSettings,
|
||||
Substitute.For<ILogger<StripeEventService>>(),
|
||||
_organizationRepository,
|
||||
_providerRepository,
|
||||
_setupIntentCache,
|
||||
_stripeFacade);
|
||||
}
|
||||
|
||||
#region GetCharge
|
||||
|
||||
17
test/Common/Helpers/CryptographyHelper.cs
Normal file
17
test/Common/Helpers/CryptographyHelper.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Bit.Test.Common.Helpers;
|
||||
|
||||
public class CryptographyHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a hex-encoded, SHA256 hash for the given string
|
||||
/// </summary>
|
||||
public static string HashAndEncode(string text)
|
||||
{
|
||||
var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(text));
|
||||
var hashEncoded = Convert.ToHexString(hashBytes).ToUpperInvariant();
|
||||
return hashEncoded;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using AutoFixture;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
|
||||
@@ -10,19 +11,30 @@ internal class PolicyCustomization : ICustomization
|
||||
{
|
||||
public PolicyType Type { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public string? Data { get; set; }
|
||||
|
||||
public PolicyCustomization(PolicyType type, bool enabled)
|
||||
public PolicyCustomization(PolicyType type, bool enabled, string? data)
|
||||
{
|
||||
Type = type;
|
||||
Enabled = enabled;
|
||||
Data = data;
|
||||
}
|
||||
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
var orgId = Guid.NewGuid();
|
||||
|
||||
fixture.Customize<Policy>(composer => composer
|
||||
.With(o => o.OrganizationId, Guid.NewGuid())
|
||||
.With(o => o.OrganizationId, orgId)
|
||||
.With(o => o.Type, Type)
|
||||
.With(o => o.Enabled, Enabled));
|
||||
.With(o => o.Enabled, Enabled)
|
||||
.With(o => o.Data, Data));
|
||||
|
||||
fixture.Customize<PolicyStatus>(composer => composer
|
||||
.With(o => o.OrganizationId, orgId)
|
||||
.With(o => o.Type, Type)
|
||||
.With(o => o.Enabled, Enabled)
|
||||
.With(o => o.Data, Data));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,15 +42,17 @@ public class PolicyAttribute : CustomizeAttribute
|
||||
{
|
||||
private readonly PolicyType _type;
|
||||
private readonly bool _enabled;
|
||||
private readonly string? _data;
|
||||
|
||||
public PolicyAttribute(PolicyType type, bool enabled = true)
|
||||
public PolicyAttribute(PolicyType type, bool enabled = true, string? data = null)
|
||||
{
|
||||
_type = type;
|
||||
_enabled = enabled;
|
||||
_data = data;
|
||||
}
|
||||
|
||||
public override ICustomization GetCustomization(ParameterInfo parameter)
|
||||
{
|
||||
return new PolicyCustomization(_type, _enabled);
|
||||
return new PolicyCustomization(_type, _enabled, _data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@@ -29,11 +31,12 @@ public class AdminRecoverAccountCommandTests
|
||||
Organization organization,
|
||||
OrganizationUser organizationUser,
|
||||
User user,
|
||||
[Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,
|
||||
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
SetupValidOrganization(sutProvider, organization);
|
||||
SetupValidPolicy(sutProvider, organization);
|
||||
SetupValidPolicy(sutProvider, organization, policy);
|
||||
SetupValidOrganizationUser(organizationUser, organization.Id);
|
||||
SetupValidUser(sutProvider, user, organizationUser);
|
||||
SetupSuccessfulPasswordUpdate(sutProvider, user, newMasterPassword);
|
||||
@@ -87,25 +90,18 @@ public class AdminRecoverAccountCommandTests
|
||||
Assert.Equal("Organization does not allow password reset.", exception.Message);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> InvalidPolicies => new object[][]
|
||||
{
|
||||
[new Policy { Type = PolicyType.ResetPassword, Enabled = false }], [null]
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[BitMemberAutoData(nameof(InvalidPolicies))]
|
||||
[BitAutoData]
|
||||
public async Task RecoverAccountAsync_InvalidPolicy_ThrowsBadRequest(
|
||||
Policy resetPasswordPolicy,
|
||||
string newMasterPassword,
|
||||
string key,
|
||||
Organization organization,
|
||||
[Policy(PolicyType.ResetPassword, false)] PolicyStatus policy,
|
||||
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
SetupValidOrganization(sutProvider, organization);
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword)
|
||||
.Returns(resetPasswordPolicy);
|
||||
SetupValidPolicy(sutProvider, organization, policy);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
@@ -171,11 +167,12 @@ public class AdminRecoverAccountCommandTests
|
||||
Organization organization,
|
||||
string newMasterPassword,
|
||||
string key,
|
||||
[Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,
|
||||
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
SetupValidOrganization(sutProvider, organization);
|
||||
SetupValidPolicy(sutProvider, organization);
|
||||
SetupValidPolicy(sutProvider, organization, policy);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
@@ -190,11 +187,12 @@ public class AdminRecoverAccountCommandTests
|
||||
string key,
|
||||
Organization organization,
|
||||
OrganizationUser organizationUser,
|
||||
[Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,
|
||||
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
SetupValidOrganization(sutProvider, organization);
|
||||
SetupValidPolicy(sutProvider, organization);
|
||||
SetupValidPolicy(sutProvider, organization, policy);
|
||||
SetupValidOrganizationUser(organizationUser, organization.Id);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByIdAsync(organizationUser.UserId!.Value)
|
||||
@@ -213,11 +211,12 @@ public class AdminRecoverAccountCommandTests
|
||||
Organization organization,
|
||||
OrganizationUser organizationUser,
|
||||
User user,
|
||||
[Policy(PolicyType.ResetPassword, true)] PolicyStatus policy,
|
||||
SutProvider<AdminRecoverAccountCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
SetupValidOrganization(sutProvider, organization);
|
||||
SetupValidPolicy(sutProvider, organization);
|
||||
SetupValidPolicy(sutProvider, organization, policy);
|
||||
SetupValidOrganizationUser(organizationUser, organization.Id);
|
||||
user.UsesKeyConnector = true;
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
@@ -238,11 +237,10 @@ public class AdminRecoverAccountCommandTests
|
||||
.Returns(organization);
|
||||
}
|
||||
|
||||
private static void SetupValidPolicy(SutProvider<AdminRecoverAccountCommand> sutProvider, Organization organization)
|
||||
private static void SetupValidPolicy(SutProvider<AdminRecoverAccountCommand> sutProvider, Organization organization, PolicyStatus policy)
|
||||
{
|
||||
var policy = new Policy { Type = PolicyType.ResetPassword, Enabled = true };
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword)
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(organization.Id, PolicyType.ResetPassword)
|
||||
.Returns(policy);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimed
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
@@ -120,7 +119,7 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
[Organization(useAutomaticUserConfirmation: true, planType: PlanType.EnterpriseAnnually)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
@@ -137,8 +136,8 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
@@ -280,7 +279,7 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
@@ -303,8 +302,8 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
PolicyType = PolicyType.TwoFactorAuthentication
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
@@ -334,7 +333,7 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
@@ -351,8 +350,8 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
@@ -389,7 +388,7 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
@@ -406,8 +405,8 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
@@ -448,7 +447,7 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
@@ -465,8 +464,8 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
@@ -501,7 +500,8 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId)
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation, false)] PolicyStatus policy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
@@ -518,9 +518,9 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns((Policy)null);
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(policy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
@@ -545,7 +545,7 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
[Organization(useAutomaticUserConfirmation: false)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
@@ -562,8 +562,8 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
@@ -589,7 +589,7 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] PolicyStatus autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
@@ -606,8 +606,8 @@ public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
@@ -9,6 +11,7 @@ using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
@@ -31,6 +34,7 @@ public class SendOrganizationInvitesCommandTests
|
||||
Organization organization,
|
||||
SsoConfig ssoConfig,
|
||||
OrganizationUser invite,
|
||||
[Policy(PolicyType.RequireSso, false)] PolicyStatus policy,
|
||||
SutProvider<SendOrganizationInvitesCommand> sutProvider)
|
||||
{
|
||||
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
|
||||
@@ -45,7 +49,9 @@ public class SendOrganizationInvitesCommandTests
|
||||
sutProvider.GetDependency<ISsoConfigRepository>().GetByOrganizationIdAsync(organization.Id).Returns(ssoConfig);
|
||||
|
||||
// Return null policy to mimic new org that's never turned on the require sso policy
|
||||
sutProvider.GetDependency<IPolicyRepository>().GetManyByOrganizationIdAsync(organization.Id).ReturnsNull();
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(organization.Id, PolicyType.RequireSso)
|
||||
.Returns(policy);
|
||||
|
||||
// Mock tokenable factory to return a token that expires in 5 days
|
||||
sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()
|
||||
|
||||
@@ -37,7 +37,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
@@ -81,7 +81,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));
|
||||
|
||||
Assert.Contains("you cannot restore yourself", exception.Message.ToLowerInvariant());
|
||||
|
||||
@@ -107,7 +107,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
RestoreUser_Setup(organization, restoringUser, organizationUser, sutProvider);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, restoringUser.Id));
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, restoringUser.Id, null));
|
||||
|
||||
Assert.Contains("only owners can restore other owners", exception.Message.ToLowerInvariant());
|
||||
|
||||
@@ -133,7 +133,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));
|
||||
|
||||
Assert.Contains("already active", exception.Message.ToLowerInvariant());
|
||||
|
||||
@@ -172,7 +172,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));
|
||||
|
||||
Assert.Contains("test@bitwarden.com belongs to an organization that doesn't allow them to join multiple organizations", exception.Message.ToLowerInvariant());
|
||||
|
||||
@@ -216,7 +216,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));
|
||||
|
||||
Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant());
|
||||
|
||||
@@ -272,7 +272,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));
|
||||
|
||||
Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant());
|
||||
|
||||
@@ -309,7 +309,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
@@ -349,7 +349,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
}
|
||||
]));
|
||||
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
@@ -395,7 +395,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));
|
||||
|
||||
Assert.Contains("test@bitwarden.com is not compliant with the single organization policy", exception.Message.ToLowerInvariant());
|
||||
|
||||
@@ -447,7 +447,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));
|
||||
|
||||
Assert.Contains("test@bitwarden.com is not compliant with the single organization and two-step login policy", exception.Message.ToLowerInvariant());
|
||||
|
||||
@@ -509,7 +509,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));
|
||||
|
||||
Assert.Contains("test@bitwarden.com is not compliant with the single organization and two-step login policy", exception.Message.ToLowerInvariant());
|
||||
|
||||
@@ -548,7 +548,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });
|
||||
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
@@ -599,7 +599,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null));
|
||||
|
||||
Assert.Equal("User is an owner/admin of another free organization. Please have them upgrade to a paid plan to restore their account.", exception.Message);
|
||||
}
|
||||
@@ -651,7 +651,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });
|
||||
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null);
|
||||
|
||||
await organizationUserRepository
|
||||
.Received(1)
|
||||
@@ -707,7 +707,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });
|
||||
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null);
|
||||
|
||||
await organizationUserRepository
|
||||
.Received(1)
|
||||
@@ -735,7 +735,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
Users = 1
|
||||
});
|
||||
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, "");
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
@@ -782,7 +782,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, new[] { orgUser1.Id, orgUser2.Id }, owner.Id, userService);
|
||||
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, new[] { orgUser1.Id, orgUser2.Id }, owner.Id, userService, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
@@ -843,7 +843,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id, orgUser2.Id, orgUser3.Id], owner.Id, userService);
|
||||
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id, orgUser2.Id, orgUser3.Id], owner.Id, userService, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, result.Count);
|
||||
@@ -914,7 +914,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id, orgUser2.Id, orgUser3.Id], owner.Id, userService);
|
||||
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id, orgUser2.Id, orgUser3.Id], owner.Id, userService, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, result.Count);
|
||||
@@ -992,7 +992,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id, orgUser2.Id, orgUser3.Id], owner.Id, userService);
|
||||
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id, orgUser2.Id, orgUser3.Id], owner.Id, userService, null);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, result.Count);
|
||||
@@ -1056,7 +1056,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id], owner.Id, userService);
|
||||
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id], owner.Id, userService, null);
|
||||
|
||||
// Assert
|
||||
Assert.Single(result);
|
||||
@@ -1107,7 +1107,7 @@ public class RestoreOrganizationUserCommandTests
|
||||
.Returns([new OrganizationUserPolicyDetails { OrganizationId = organization.Id, PolicyType = PolicyType.TwoFactorAuthentication }]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id], owner.Id, userService);
|
||||
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id], owner.Id, userService, null);
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(string.Empty, result[0].Item2);
|
||||
@@ -1138,5 +1138,408 @@ public class RestoreOrganizationUserCommandTests
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(requestingOrganizationUser != null && requestingOrganizationUser.Type is OrganizationUserType.Owner);
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(requestingOrganizationUser != null && (requestingOrganizationUser.Type is OrganizationUserType.Owner or OrganizationUserType.Admin));
|
||||
|
||||
// Setup default disabled OrganizationDataOwnershipPolicyRequirement for any user
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(Arg.Any<Guid>())
|
||||
.Returns(new OrganizationDataOwnershipPolicyRequirement(OrganizationDataOwnershipState.Disabled, []));
|
||||
}
|
||||
|
||||
private static void SetupOrganizationDataOwnershipPolicy(
|
||||
SutProvider<RestoreOrganizationUserCommand> sutProvider,
|
||||
Guid userId,
|
||||
Guid organizationId,
|
||||
OrganizationUserStatusType orgUserStatus,
|
||||
bool policyEnabled)
|
||||
{
|
||||
var policyDetails = policyEnabled
|
||||
? new List<PolicyDetails>
|
||||
{
|
||||
new()
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
OrganizationUserId = Guid.NewGuid(),
|
||||
OrganizationUserStatus = orgUserStatus,
|
||||
PolicyType = PolicyType.OrganizationDataOwnership
|
||||
}
|
||||
}
|
||||
: new List<PolicyDetails>();
|
||||
|
||||
var policyRequirement = new OrganizationDataOwnershipPolicyRequirement(
|
||||
policyEnabled ? OrganizationDataOwnershipState.Enabled : OrganizationDataOwnershipState.Disabled,
|
||||
policyDetails);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)
|
||||
.Returns(policyRequirement);
|
||||
}
|
||||
|
||||
#region Single User Restore - Default Collection Tests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_WithDataOwnershipPolicyEnabled_AndConfirmedUser_CreatesDefaultCollection(
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||
string defaultCollectionName,
|
||||
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.Email = null; // This causes user to restore to Confirmed status
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.DefaultUserCollectionRestore)
|
||||
.Returns(true);
|
||||
|
||||
SetupOrganizationDataOwnershipPolicy(
|
||||
sutProvider,
|
||||
organizationUser.UserId!.Value,
|
||||
organization.Id,
|
||||
OrganizationUserStatusType.Revoked,
|
||||
policyEnabled: true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, defaultCollectionName);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.Received(1)
|
||||
.CreateDefaultCollectionsAsync(
|
||||
organization.Id,
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Single() == organizationUser.Id),
|
||||
defaultCollectionName);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_WithDataOwnershipPolicyDisabled_DoesNotCreateDefaultCollection(
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||
string defaultCollectionName,
|
||||
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.Email = null; // This causes user to restore to Confirmed status
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.DefaultUserCollectionRestore)
|
||||
.Returns(true);
|
||||
|
||||
SetupOrganizationDataOwnershipPolicy(
|
||||
sutProvider,
|
||||
organizationUser.UserId!.Value,
|
||||
organization.Id,
|
||||
OrganizationUserStatusType.Revoked,
|
||||
policyEnabled: false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, defaultCollectionName);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_WithNullDefaultCollectionName_DoesNotCreateDefaultCollection(
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.Email = null; // This causes user to restore to Confirmed status
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.DefaultUserCollectionRestore)
|
||||
.Returns(true);
|
||||
|
||||
SetupOrganizationDataOwnershipPolicy(
|
||||
sutProvider,
|
||||
organizationUser.UserId!.Value,
|
||||
organization.Id,
|
||||
OrganizationUserStatusType.Revoked,
|
||||
policyEnabled: true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, null);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("")]
|
||||
[BitAutoData(" ")]
|
||||
public async Task RestoreUser_WithEmptyOrWhitespaceDefaultCollectionName_DoesNotCreateDefaultCollection(
|
||||
string defaultCollectionName,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.Email = null; // This causes user to restore to Confirmed status
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.DefaultUserCollectionRestore)
|
||||
.Returns(true);
|
||||
|
||||
SetupOrganizationDataOwnershipPolicy(
|
||||
sutProvider,
|
||||
organizationUser.UserId!.Value,
|
||||
organization.Id,
|
||||
OrganizationUserStatusType.Revoked,
|
||||
policyEnabled: true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, defaultCollectionName);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_UserRestoredToInvitedStatus_DoesNotCreateDefaultCollection(
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||
string defaultCollectionName,
|
||||
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = PlanType.EnterpriseAnnually; // Non-Free plan to avoid ownership check requiring UserId
|
||||
organizationUser.Email = "test@example.com"; // Non-null email means user restores to Invited status
|
||||
organizationUser.UserId = null; // User not linked to account yet
|
||||
organizationUser.Key = null;
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.DefaultUserCollectionRestore)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, defaultCollectionName);
|
||||
|
||||
// Assert - User was restored to Invited status, so no collection should be created
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_WithNoUserId_DoesNotCreateDefaultCollection(
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||
string defaultCollectionName,
|
||||
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
organization.PlanType = PlanType.EnterpriseAnnually; // Non-Free plan to avoid ownership check requiring UserId
|
||||
organizationUser.UserId = null; // No linked user account
|
||||
organizationUser.Email = "test@example.com";
|
||||
organizationUser.Key = null;
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.DefaultUserCollectionRestore)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, defaultCollectionName);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Bulk User Restore - Default Collection Tests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUsers_Bulk_WithDataOwnershipPolicy_CreatesCollectionsForEligibleUsers(
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,
|
||||
string defaultCollectionName,
|
||||
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
RestoreUser_Setup(organization, owner, orgUser1, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var userService = Substitute.For<IUserService>();
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.DefaultUserCollectionRestore)
|
||||
.Returns(true);
|
||||
|
||||
// orgUser1: Will restore to Confirmed (Email = null)
|
||||
orgUser1.Email = null;
|
||||
orgUser1.OrganizationId = organization.Id;
|
||||
|
||||
// orgUser2: Will restore to Invited (Email not null)
|
||||
orgUser2.Email = "test@example.com";
|
||||
orgUser2.UserId = null;
|
||||
orgUser2.Key = null;
|
||||
orgUser2.OrganizationId = organization.Id;
|
||||
|
||||
organizationUserRepository
|
||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)))
|
||||
.Returns([orgUser1, orgUser2]);
|
||||
|
||||
// Setup bulk policy query - returns org user IDs with policy enabled
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetManyByOrganizationIdAsync<OrganizationDataOwnershipPolicyRequirement>(organization.Id)
|
||||
.Returns([orgUser1.Id]);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>
|
||||
{
|
||||
(orgUser1.UserId!.Value, true)
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RestoreUsersAsync(
|
||||
organization.Id,
|
||||
[orgUser1.Id, orgUser2.Id],
|
||||
owner.Id,
|
||||
userService,
|
||||
defaultCollectionName);
|
||||
|
||||
// Assert - Only orgUser1 should have a collection created (Confirmed with policy enabled)
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.Received(1)
|
||||
.CreateDefaultCollectionsAsync(
|
||||
organization.Id,
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Single() == orgUser1.Id),
|
||||
defaultCollectionName);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUsers_Bulk_WithMixedPolicyStates_OnlyCreatesForEnabledPolicy(
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,
|
||||
string defaultCollectionName,
|
||||
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
RestoreUser_Setup(organization, owner, orgUser1, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var userService = Substitute.For<IUserService>();
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.DefaultUserCollectionRestore)
|
||||
.Returns(true);
|
||||
|
||||
// Both users will restore to Confirmed
|
||||
orgUser1.Email = null;
|
||||
orgUser1.OrganizationId = organization.Id;
|
||||
orgUser2.Email = null;
|
||||
orgUser2.OrganizationId = organization.Id;
|
||||
|
||||
organizationUserRepository
|
||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)))
|
||||
.Returns([orgUser1, orgUser2]);
|
||||
|
||||
// Setup bulk policy query - only orgUser1 has policy enabled
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetManyByOrganizationIdAsync<OrganizationDataOwnershipPolicyRequirement>(organization.Id)
|
||||
.Returns([orgUser1.Id]);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>
|
||||
{
|
||||
(orgUser1.UserId!.Value, true),
|
||||
(orgUser2.UserId!.Value, true)
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RestoreUsersAsync(
|
||||
organization.Id,
|
||||
[orgUser1.Id, orgUser2.Id],
|
||||
owner.Id,
|
||||
userService,
|
||||
defaultCollectionName);
|
||||
|
||||
// Assert - Only orgUser1 should have a collection created (policy enabled)
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.Received(1)
|
||||
.CreateDefaultCollectionsAsync(
|
||||
organization.Id,
|
||||
Arg.Is<IEnumerable<Guid>>(ids => ids.Single() == orgUser1.Id),
|
||||
defaultCollectionName);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUsers_Bulk_WithNullCollectionName_DoesNotCreateAnyCollections(
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,
|
||||
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
RestoreUser_Setup(organization, owner, orgUser1, sutProvider);
|
||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||
var userService = Substitute.For<IUserService>();
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.DefaultUserCollectionRestore)
|
||||
.Returns(true);
|
||||
|
||||
// Both users will restore to Confirmed
|
||||
orgUser1.Email = null;
|
||||
orgUser1.OrganizationId = organization.Id;
|
||||
orgUser2.Email = null;
|
||||
orgUser2.OrganizationId = organization.Id;
|
||||
|
||||
organizationUserRepository
|
||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)))
|
||||
.Returns([orgUser1, orgUser2]);
|
||||
|
||||
// Setup bulk policy query - both users have policy enabled
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetManyByOrganizationIdAsync<OrganizationDataOwnershipPolicyRequirement>(organization.Id)
|
||||
.Returns([orgUser1.Id, orgUser2.Id]);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>
|
||||
{
|
||||
(orgUser1.UserId!.Value, true),
|
||||
(orgUser2.UserId!.Value, true)
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RestoreUsersAsync(
|
||||
organization.Id,
|
||||
[orgUser1.Id, orgUser2.Id],
|
||||
owner.Id,
|
||||
userService,
|
||||
null); // Null collection name
|
||||
|
||||
// Assert - No collections should be created
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.CreateDefaultCollectionsAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -19,12 +19,17 @@ public class PolicyDataValidatorTests
|
||||
[Fact]
|
||||
public void ValidateAndSerialize_ValidData_ReturnsSerializedJson()
|
||||
{
|
||||
var data = new Dictionary<string, object> { { "minLength", 12 } };
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
{ "minLength", 12 },
|
||||
{ "minComplexity", 4 }
|
||||
};
|
||||
|
||||
var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("\"minLength\":12", result);
|
||||
Assert.Contains("\"minComplexity\":4", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -56,4 +61,122 @@ public class PolicyDataValidatorTests
|
||||
|
||||
Assert.IsType<OrganizationModelOwnershipPolicyModel>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAndSerialize_ExcessiveMinLength_ThrowsBadRequestException()
|
||||
{
|
||||
var data = new Dictionary<string, object> { { "minLength", 129 } };
|
||||
|
||||
var exception = Assert.Throws<BadRequestException>(() =>
|
||||
PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));
|
||||
|
||||
Assert.Contains("Invalid data for MasterPassword policy", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAndSerialize_ExcessiveMinComplexity_ThrowsBadRequestException()
|
||||
{
|
||||
var data = new Dictionary<string, object> { { "minComplexity", 5 } };
|
||||
|
||||
var exception = Assert.Throws<BadRequestException>(() =>
|
||||
PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));
|
||||
|
||||
Assert.Contains("Invalid data for MasterPassword policy", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAndSerialize_MinLengthAtMinimum_Succeeds()
|
||||
{
|
||||
var data = new Dictionary<string, object> { { "minLength", 12 } };
|
||||
|
||||
var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("\"minLength\":12", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAndSerialize_MinLengthAtMaximum_Succeeds()
|
||||
{
|
||||
var data = new Dictionary<string, object> { { "minLength", 128 } };
|
||||
|
||||
var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("\"minLength\":128", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAndSerialize_MinLengthBelowMinimum_ThrowsBadRequestException()
|
||||
{
|
||||
var data = new Dictionary<string, object> { { "minLength", 11 } };
|
||||
|
||||
var exception = Assert.Throws<BadRequestException>(() =>
|
||||
PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));
|
||||
|
||||
Assert.Contains("Invalid data for MasterPassword policy", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAndSerialize_MinComplexityAtMinimum_Succeeds()
|
||||
{
|
||||
var data = new Dictionary<string, object> { { "minComplexity", 0 } };
|
||||
|
||||
var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("\"minComplexity\":0", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAndSerialize_MinComplexityAtMaximum_Succeeds()
|
||||
{
|
||||
var data = new Dictionary<string, object> { { "minComplexity", 4 } };
|
||||
|
||||
var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("\"minComplexity\":4", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAndSerialize_MinComplexityBelowMinimum_ThrowsBadRequestException()
|
||||
{
|
||||
var data = new Dictionary<string, object> { { "minComplexity", -1 } };
|
||||
|
||||
var exception = Assert.Throws<BadRequestException>(() =>
|
||||
PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));
|
||||
|
||||
Assert.Contains("Invalid data for MasterPassword policy", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAndSerialize_NullMinLength_Succeeds()
|
||||
{
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
{ "minComplexity", 2 }
|
||||
// minLength is omitted, should be null
|
||||
};
|
||||
|
||||
var result = PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Contains("\"minComplexity\":2", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateAndSerialize_MultipleInvalidFields_ThrowsBadRequestException()
|
||||
{
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
{ "minLength", 200 },
|
||||
{ "minComplexity", 10 }
|
||||
};
|
||||
|
||||
var exception = Assert.Throws<BadRequestException>(() =>
|
||||
PolicyDataValidator.ValidateAndSerialize(data, PolicyType.MasterPassword));
|
||||
|
||||
Assert.Contains("Invalid data for MasterPassword policy", exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
@@ -13,6 +13,7 @@ using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@@ -163,7 +164,8 @@ public class SsoConfigServiceTests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_KeyConnector_SingleOrgNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,
|
||||
Organization organization)
|
||||
Organization organization,
|
||||
[Policy(PolicyType.SingleOrg, false)] PolicyStatus policy)
|
||||
{
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
@@ -180,6 +182,9 @@ public class SsoConfigServiceTests
|
||||
RevisionDate = utcNow.AddDays(-10),
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyQuery>().RunAsync(
|
||||
Arg.Any<Guid>(), PolicyType.SingleOrg).Returns(policy);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(ssoConfig, organization));
|
||||
|
||||
@@ -191,7 +196,9 @@ public class SsoConfigServiceTests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_KeyConnector_SsoPolicyNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,
|
||||
Organization organization)
|
||||
Organization organization,
|
||||
[Policy(PolicyType.SingleOrg, true)] PolicyStatus singleOrgPolicy,
|
||||
[Policy(PolicyType.RequireSso, false)] PolicyStatus requireSsoPolicy)
|
||||
{
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
@@ -208,11 +215,10 @@ public class SsoConfigServiceTests
|
||||
RevisionDate = utcNow.AddDays(-10),
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>().GetByOrganizationIdTypeAsync(
|
||||
Arg.Any<Guid>(), PolicyType.SingleOrg).Returns(new Policy
|
||||
{
|
||||
Enabled = true
|
||||
});
|
||||
sutProvider.GetDependency<IPolicyQuery>().RunAsync(
|
||||
Arg.Any<Guid>(), PolicyType.SingleOrg).Returns(singleOrgPolicy);
|
||||
sutProvider.GetDependency<IPolicyQuery>().RunAsync(
|
||||
Arg.Any<Guid>(), PolicyType.RequireSso).Returns(requireSsoPolicy);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(ssoConfig, organization));
|
||||
@@ -225,7 +231,8 @@ public class SsoConfigServiceTests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_KeyConnector_SsoConfigNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,
|
||||
Organization organization)
|
||||
Organization organization,
|
||||
[Policy(PolicyType.SingleOrg, true)] PolicyStatus policy)
|
||||
{
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
@@ -242,11 +249,8 @@ public class SsoConfigServiceTests
|
||||
RevisionDate = utcNow.AddDays(-10),
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>().GetByOrganizationIdTypeAsync(
|
||||
Arg.Any<Guid>(), Arg.Any<PolicyType>()).Returns(new Policy
|
||||
{
|
||||
Enabled = true
|
||||
});
|
||||
sutProvider.GetDependency<IPolicyQuery>().RunAsync(
|
||||
Arg.Any<Guid>(), Arg.Any<PolicyType>()).Returns(policy);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(ssoConfig, organization));
|
||||
@@ -259,7 +263,8 @@ public class SsoConfigServiceTests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_KeyConnector_KeyConnectorAbilityNotEnabled_Throws(SutProvider<SsoConfigService> sutProvider,
|
||||
Organization organization)
|
||||
Organization organization,
|
||||
[Policy(PolicyType.SingleOrg, true)] PolicyStatus policy)
|
||||
{
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
@@ -277,11 +282,8 @@ public class SsoConfigServiceTests
|
||||
RevisionDate = utcNow.AddDays(-10),
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>().GetByOrganizationIdTypeAsync(
|
||||
Arg.Any<Guid>(), Arg.Any<PolicyType>()).Returns(new Policy
|
||||
{
|
||||
Enabled = true,
|
||||
});
|
||||
sutProvider.GetDependency<IPolicyQuery>().RunAsync(
|
||||
Arg.Any<Guid>(), Arg.Any<PolicyType>()).Returns(policy);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(ssoConfig, organization));
|
||||
@@ -294,7 +296,8 @@ public class SsoConfigServiceTests
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_KeyConnector_Success(SutProvider<SsoConfigService> sutProvider,
|
||||
Organization organization)
|
||||
Organization organization,
|
||||
[Policy(PolicyType.SingleOrg, true)] PolicyStatus policy)
|
||||
{
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
@@ -312,11 +315,8 @@ public class SsoConfigServiceTests
|
||||
RevisionDate = utcNow.AddDays(-10),
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>().GetByOrganizationIdTypeAsync(
|
||||
Arg.Any<Guid>(), Arg.Any<PolicyType>()).Returns(new Policy
|
||||
{
|
||||
Enabled = true,
|
||||
});
|
||||
sutProvider.GetDependency<IPolicyQuery>().RunAsync(
|
||||
Arg.Any<Guid>(), Arg.Any<PolicyType>()).Returns(policy);
|
||||
|
||||
await sutProvider.Sut.SaveAsync(ssoConfig, organization);
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Text;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
@@ -13,6 +14,7 @@ using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterpri
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
@@ -241,7 +243,8 @@ public class RegisterUserCommandTests
|
||||
[BitAutoData(true, "sampleInitiationPath")]
|
||||
[BitAutoData(true, "Secrets Manager trial")]
|
||||
public async Task RegisterUserViaOrganizationInviteToken_ComplexHappyPath_Succeeds(bool addUserReferenceData, string initiationPath,
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId, Policy twoFactorPolicy)
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId,
|
||||
[Policy(PolicyType.TwoFactorAuthentication, true)] PolicyStatus policy)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IGlobalSettings>()
|
||||
@@ -267,10 +270,9 @@ public class RegisterUserCommandTests
|
||||
.GetByIdAsync(orgUserId)
|
||||
.Returns(orgUser);
|
||||
|
||||
twoFactorPolicy.Enabled = true;
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(orgUser.OrganizationId, PolicyType.TwoFactorAuthentication)
|
||||
.Returns(twoFactorPolicy);
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(orgUser.OrganizationId, PolicyType.TwoFactorAuthentication)
|
||||
.Returns(policy);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CreateUserAsync(user, masterPasswordHash)
|
||||
@@ -286,9 +288,9 @@ public class RegisterUserCommandTests
|
||||
.Received(1)
|
||||
.GetByIdAsync(orgUserId);
|
||||
|
||||
await sutProvider.GetDependency<IPolicyRepository>()
|
||||
await sutProvider.GetDependency<IPolicyQuery>()
|
||||
.Received(1)
|
||||
.GetByOrganizationIdTypeAsync(orgUser.OrganizationId, PolicyType.TwoFactorAuthentication);
|
||||
.RunAsync(orgUser.OrganizationId, PolicyType.TwoFactorAuthentication);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.Received(1)
|
||||
@@ -431,7 +433,8 @@ public class RegisterUserCommandTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaOrganizationInviteToken_BlockedDomainFromDifferentOrg_ThrowsBadRequestException(
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId)
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId,
|
||||
[Policy(PolicyType.TwoFactorAuthentication, false)] PolicyStatus policy)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = "user@blocked-domain.com";
|
||||
@@ -463,6 +466,10 @@ public class RegisterUserCommandTests
|
||||
.HasVerifiedDomainWithBlockClaimedDomainPolicyAsync("blocked-domain.com", orgUser.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)
|
||||
.Returns(policy);
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId));
|
||||
@@ -472,7 +479,8 @@ public class RegisterUserCommandTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaOrganizationInviteToken_BlockedDomainFromSameOrg_Succeeds(
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId)
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, OrganizationUser orgUser, string orgInviteToken, Guid orgUserId,
|
||||
[Policy(PolicyType.TwoFactorAuthentication, false)] PolicyStatus policy)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = "user@company-domain.com";
|
||||
@@ -509,6 +517,10 @@ public class RegisterUserCommandTests
|
||||
.CreateUserAsync(user, masterPasswordHash)
|
||||
.Returns(IdentityResult.Success);
|
||||
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)
|
||||
.Returns(policy);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RegisterUserViaOrganizationInviteToken(user, masterPasswordHash, orgInviteToken, orgUserId);
|
||||
|
||||
@@ -1245,6 +1257,7 @@ public class RegisterUserCommandTests
|
||||
OrganizationUser orgUser,
|
||||
string orgInviteToken,
|
||||
string masterPasswordHash,
|
||||
[Policy(PolicyType.TwoFactorAuthentication, false)] PolicyStatus policy,
|
||||
SutProvider<RegisterUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
@@ -1259,9 +1272,9 @@ public class RegisterUserCommandTests
|
||||
.GetByIdAsync(orgUser.Id)
|
||||
.Returns(orgUser);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)
|
||||
.Returns((Policy)null);
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)
|
||||
.Returns(policy);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(orgUser.OrganizationId)
|
||||
@@ -1331,6 +1344,7 @@ public class RegisterUserCommandTests
|
||||
OrganizationUser orgUser,
|
||||
string masterPasswordHash,
|
||||
string orgInviteToken,
|
||||
[Policy(PolicyType.TwoFactorAuthentication, false)] PolicyStatus policy,
|
||||
SutProvider<RegisterUserCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
@@ -1346,9 +1360,9 @@ public class RegisterUserCommandTests
|
||||
.GetByIdAsync(orgUser.Id)
|
||||
.Returns(orgUser);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)
|
||||
.Returns((Policy)null);
|
||||
sutProvider.GetDependency<IPolicyQuery>()
|
||||
.RunAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication)
|
||||
.Returns(policy);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(orgUser.OrganizationId)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user