mirror of
https://github.com/bitwarden/server.git
synced 2026-01-31 06:03:12 +08:00
Merge branch 'main' into billing/PM-29599/upgrade-proration
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>
|
||||
|
||||
@@ -23,11 +23,9 @@
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
},
|
||||
"Console": {
|
||||
"IncludeScopes": true,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,6 +39,14 @@
|
||||
},
|
||||
"licenseDirectory": "<full path to license directory>",
|
||||
"enableNewDeviceVerification": true,
|
||||
"enableEmailVerification": true
|
||||
"enableEmailVerification": true,
|
||||
"communication": {
|
||||
"bootstrap": "none",
|
||||
"ssoCookieVendor": {
|
||||
"idpLoginUrl": "",
|
||||
"cookieName": "",
|
||||
"cookieDomain": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ $projects = @{
|
||||
Scim = "../bitwarden_license/src/Scim"
|
||||
IntegrationTests = "../test/Infrastructure.IntegrationTest"
|
||||
SeederApi = "../util/SeederApi"
|
||||
SeederUtility = "../util/DbSeederUtility"
|
||||
}
|
||||
|
||||
foreach ($key in $projects.keys) {
|
||||
|
||||
@@ -20,11 +20,9 @@
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
},
|
||||
"Console": {
|
||||
"IncludeScopes": true,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ using Bit.Api.Auth.Models.Request;
|
||||
using Bit.Api.Auth.Models.Response;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Vault.Models.Response;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Auth.UserFeatures.EmergencyAccess;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Auth.UserFeatures.EmergencyAccess;
|
||||
using Bit.Core.Jobs;
|
||||
using Quartz;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Auth.UserFeatures.EmergencyAccess;
|
||||
using Bit.Core.Jobs;
|
||||
using Quartz;
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -23,11 +23,9 @@
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"System": "Information",
|
||||
"Microsoft": "Information"
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
},
|
||||
"Console": {
|
||||
"IncludeScopes": true,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,17 +275,24 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
.PreviousAttributes
|
||||
.ToObject<Subscription>() as Subscription;
|
||||
|
||||
// Get all plan IDs that include Secrets Manager support to check if the organization has secret manager in the
|
||||
// previous and/or current subscriptions.
|
||||
var planIdsOfPlansWithSecretManager = (await _pricingClient.ListPlans())
|
||||
.Where(orgPlan => orgPlan.SupportsSecretsManager && orgPlan.SecretsManager.StripeSeatPlanId != null)
|
||||
.Select(orgPlan => orgPlan.SecretsManager.StripeSeatPlanId)
|
||||
.ToHashSet();
|
||||
|
||||
// This being false doesn't necessarily mean that the organization doesn't subscribe to Secrets Manager.
|
||||
// If there are changes to any subscription item, Stripe sends every item in the subscription, both
|
||||
// changed and unchanged.
|
||||
var previousSubscriptionHasSecretsManager =
|
||||
previousSubscription?.Items is not null &&
|
||||
previousSubscription.Items.Any(
|
||||
previousSubscriptionItem => previousSubscriptionItem.Plan.Id == plan.SecretsManager.StripeSeatPlanId);
|
||||
previousSubscriptionItem => planIdsOfPlansWithSecretManager.Contains(previousSubscriptionItem.Plan.Id));
|
||||
|
||||
var currentSubscriptionHasSecretsManager =
|
||||
subscription.Items.Any(
|
||||
currentSubscriptionItem => currentSubscriptionItem.Plan.Id == plan.SecretsManager.StripeSeatPlanId);
|
||||
currentSubscriptionItem => planIdsOfPlansWithSecretManager.Contains(currentSubscriptionItem.Plan.Id));
|
||||
|
||||
if (!previousSubscriptionHasSecretsManager || currentSubscriptionHasSecretsManager)
|
||||
{
|
||||
|
||||
@@ -627,7 +627,7 @@ public class UpcomingInvoiceHandler(
|
||||
{
|
||||
BaseMonthlyRenewalPrice = (premiumPlan.Seat.Price / 12).ToString("C", new CultureInfo("en-US")),
|
||||
DiscountAmount = $"{coupon.PercentOff}%",
|
||||
DiscountedMonthlyRenewalPrice = (discountedAnnualRenewalPrice / 12).ToString("C", new CultureInfo("en-US"))
|
||||
DiscountedAnnualRenewalPrice = discountedAnnualRenewalPrice.ToString("C", new CultureInfo("en-US"))
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
@@ -93,7 +94,7 @@ public class RestoreOrganizationUserCommand(
|
||||
.twoFactorIsEnabled;
|
||||
}
|
||||
|
||||
if (organization.PlanType == PlanType.Free)
|
||||
if (organization.PlanType == PlanType.Free && organizationUser.UserId.HasValue)
|
||||
{
|
||||
await CheckUserForOtherFreeOrganizationOwnershipAsync(organizationUser);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -21,7 +21,9 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
|
||||
Task<IEnumerable<string>> GetOwnerEmailAddressesById(Guid organizationId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the organizations that have a verified domain matching the user's email domain.
|
||||
/// Gets the organizations that have claimed the user's account. Currently, only one organization may claim a user.
|
||||
/// This requires that the organization has claimed the user's domain and the user is an organization member.
|
||||
/// It excludes invited members.
|
||||
/// </summary>
|
||||
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
|
||||
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
@@ -19,7 +18,7 @@ using Bit.Core.Vault.Models.Data;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Bit.Core.Vault.Services;
|
||||
|
||||
namespace Bit.Core.Auth.Services;
|
||||
namespace Bit.Core.Auth.UserFeatures.EmergencyAccess;
|
||||
|
||||
public class EmergencyAccessService : IEmergencyAccessService
|
||||
{
|
||||
@@ -61,7 +60,7 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
}
|
||||
|
||||
public async Task<EmergencyAccess> InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime)
|
||||
public async Task<Entities.EmergencyAccess> InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime)
|
||||
{
|
||||
if (!await _userService.CanAccessPremium(grantorUser))
|
||||
{
|
||||
@@ -73,7 +72,7 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
throw new BadRequestException("You cannot use Emergency Access Takeover because you are using Key Connector.");
|
||||
}
|
||||
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
var emergencyAccess = new Entities.EmergencyAccess
|
||||
{
|
||||
GrantorId = grantorUser.Id,
|
||||
Email = emergencyContactEmail.ToLowerInvariant(),
|
||||
@@ -113,7 +112,7 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
await SendInviteAsync(emergencyAccess, NameOrEmail(grantorUser));
|
||||
}
|
||||
|
||||
public async Task<EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService)
|
||||
public async Task<Entities.EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
|
||||
if (emergencyAccess == null)
|
||||
@@ -175,7 +174,7 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
await _emergencyAccessRepository.DeleteAsync(emergencyAccess);
|
||||
}
|
||||
|
||||
public async Task<EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId)
|
||||
public async Task<Entities.EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
|
||||
if (emergencyAccess == null || emergencyAccess.Status != EmergencyAccessStatusType.Accepted ||
|
||||
@@ -201,7 +200,7 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
return emergencyAccess;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(EmergencyAccess emergencyAccess, User grantorUser)
|
||||
public async Task SaveAsync(Entities.EmergencyAccess emergencyAccess, User grantorUser)
|
||||
{
|
||||
if (!await _userService.CanAccessPremium(grantorUser))
|
||||
{
|
||||
@@ -311,7 +310,7 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
}
|
||||
|
||||
// TODO PM-21687: rename this to something like InitiateRecoveryTakeoverAsync
|
||||
public async Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser)
|
||||
public async Task<(Entities.EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
|
||||
|
||||
@@ -429,7 +428,7 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
return await _cipherService.GetAttachmentDownloadDataAsync(cipher, attachmentId);
|
||||
}
|
||||
|
||||
private async Task SendInviteAsync(EmergencyAccess emergencyAccess, string invitingUsersName)
|
||||
private async Task SendInviteAsync(Entities.EmergencyAccess emergencyAccess, string invitingUsersName)
|
||||
{
|
||||
var token = _dataProtectorTokenizer.Protect(new EmergencyAccessInviteTokenable(emergencyAccess, _globalSettings.OrganizationInviteExpirationHours));
|
||||
await _mailService.SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUsersName, token);
|
||||
@@ -449,7 +448,7 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
*/
|
||||
//TODO PM-21687: this IsValidRequest() checks the validity based on the granteeUser. There should be a complementary method for the grantorUser
|
||||
private static bool IsValidRequest(
|
||||
EmergencyAccess availableAccess,
|
||||
Entities.EmergencyAccess availableAccess,
|
||||
User requestingUser,
|
||||
EmergencyAccessType requestedAccessType)
|
||||
{
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
@@ -7,7 +6,7 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
|
||||
namespace Bit.Core.Auth.Services;
|
||||
namespace Bit.Core.Auth.UserFeatures.EmergencyAccess;
|
||||
|
||||
public interface IEmergencyAccessService
|
||||
{
|
||||
@@ -20,7 +19,7 @@ public interface IEmergencyAccessService
|
||||
/// <param name="accessType">Type of emergency access allowed to the emergency contact</param>
|
||||
/// <param name="waitTime">The amount of time to pass before the invite is auto confirmed</param>
|
||||
/// <returns>a new Emergency Access object</returns>
|
||||
Task<EmergencyAccess> InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime);
|
||||
Task<Entities.EmergencyAccess> InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime);
|
||||
/// <summary>
|
||||
/// Sends an invite to the emergency contact associated with the emergency access id.
|
||||
/// </summary>
|
||||
@@ -37,7 +36,7 @@ public interface IEmergencyAccessService
|
||||
/// <param name="token">the tokenable that was sent via email</param>
|
||||
/// <param name="userService">service dependency</param>
|
||||
/// <returns>void</returns>
|
||||
Task<EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService);
|
||||
Task<Entities.EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService);
|
||||
/// <summary>
|
||||
/// The creator of the emergency access request can delete the request.
|
||||
/// </summary>
|
||||
@@ -53,7 +52,7 @@ public interface IEmergencyAccessService
|
||||
/// <param name="key">The grantor user key encrypted by the grantee public key; grantee.PubicKey(grantor.User.Key)</param>
|
||||
/// <param name="grantorId">Id of grantor user</param>
|
||||
/// <returns>emergency access object associated with the Id passed in</returns>
|
||||
Task<EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId);
|
||||
Task<Entities.EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId);
|
||||
/// <summary>
|
||||
/// Fetches an emergency access object. The grantor user must own the object being fetched.
|
||||
/// </summary>
|
||||
@@ -67,7 +66,7 @@ public interface IEmergencyAccessService
|
||||
/// <param name="emergencyAccess">emergency access entity being updated</param>
|
||||
/// <param name="grantorUser">grantor user</param>
|
||||
/// <returns>void</returns>
|
||||
Task SaveAsync(EmergencyAccess emergencyAccess, User grantorUser);
|
||||
Task SaveAsync(Entities.EmergencyAccess emergencyAccess, User grantorUser);
|
||||
/// <summary>
|
||||
/// Initiates the recovery process. For either Takeover or view. Will send an email to the Grantor User notifying of the initiation.
|
||||
/// </summary>
|
||||
@@ -107,7 +106,7 @@ public interface IEmergencyAccessService
|
||||
/// <param name="emergencyAccessId">Id of entity being accessed</param>
|
||||
/// <param name="granteeUser">grantee user of the emergency access entity</param>
|
||||
/// <returns>emergency access entity and the grantorUser</returns>
|
||||
Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser);
|
||||
Task<(Entities.EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser);
|
||||
/// <summary>
|
||||
/// Updates the grantor's password hash and updates the key for the EmergencyAccess entity.
|
||||
/// </summary>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.EmergencyAccess.Mail;
|
||||
|
||||
public class EmergencyAccessRemoveGranteesMailView : BaseMailView
|
||||
{
|
||||
public required IEnumerable<string> RemovedGranteeNames { get; set; }
|
||||
public string EmergencyAccessHelpPageUrl => "https://bitwarden.com/help/emergency-access/";
|
||||
}
|
||||
|
||||
public class EmergencyAccessRemoveGranteesMail : BaseMail<EmergencyAccessRemoveGranteesMailView>
|
||||
{
|
||||
public override string Subject { get; set; } = "Emergency contacts removed";
|
||||
}
|
||||
@@ -0,0 +1,499 @@
|
||||
<!doctype html>
|
||||
<html lang="und" dir="auto" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<title></title>
|
||||
<!--[if !mso]><!-->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type="text/css">
|
||||
#outlook a { padding:0; }
|
||||
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
|
||||
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
|
||||
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
|
||||
p { display:block;margin:13px 0; }
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG/>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.mj-outlook-group-fix { width:100% !important; }
|
||||
</style>
|
||||
<![endif]-->
|
||||
|
||||
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width:480px) {
|
||||
.mj-column-per-70 { width:70% !important; max-width: 70%; }
|
||||
.mj-column-per-30 { width:30% !important; max-width: 30%; }
|
||||
.mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||
}
|
||||
</style>
|
||||
<style media="screen and (min-width:480px)">
|
||||
.moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }
|
||||
.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }
|
||||
.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
<style type="text/css">
|
||||
|
||||
@media only screen and (max-width:480px) {
|
||||
.mj-bw-hero-responsive-img {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width:479px) {
|
||||
table.mj-full-width-mobile { width: 100% !important; }
|
||||
td.mj-full-width-mobile { width: auto !important; }
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style type="text/css">
|
||||
.border-fix > table {
|
||||
border-collapse: separate !important;
|
||||
}
|
||||
.border-fix > table > tbody > tr > td {
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body style="word-spacing:normal;background-color:#e6e9ef;">
|
||||
|
||||
|
||||
<div class="border-fix" style="background-color:#e6e9ef;" lang="und" dir="auto">
|
||||
<!-- Blue Header Section -->
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="border-fix-outlook" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div class="border-fix" style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 20px 0px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><![endif]-->
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#175ddc" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;border-radius:4px 4px 0px 0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:434px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-70 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:150px;">
|
||||
|
||||
<img alt src="https://bitwarden.com/images/logo-horizontal-white.png" style="border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;" width="150" height="30">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;"><h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
|
||||
|
||||
</h1></div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td><td class="" style="vertical-align:bottom;width:186px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-30 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:bottom;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="center" class="mj-bw-hero-responsive-img" style="font-size:0px;padding:0px;word-break:break-word;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width:155px;">
|
||||
|
||||
<img alt src="undefined" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;" width="155" height="auto">
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
<!-- Main Content -->
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px 20px 0px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px 10px 0px 10px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 15px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">The following emergency contacts have been removed from your account:
|
||||
<ul>
|
||||
{{#each RemovedGranteeNames}}
|
||||
<li>{{this}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
Learn more about <a href="{{EmergencyAccessHelpPageUrl}}">emergency access</a>.</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
<!-- Footer -->
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="margin:0px auto;max-width:660px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:620px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:0;word-break:break-word;">
|
||||
|
||||
|
||||
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" ><tr><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://x.com/bitwarden" target="_blank">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.reddit.com/r/Bitwarden/" target="_blank">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-reddit.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://community.bitwarden.com/" target="_blank">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-discourse.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/bitwarden" target="_blank">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-github.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-youtube.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.linkedin.com/company/bitwarden1/" target="_blank">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-linkedin.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td><td><![endif]-->
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.facebook.com/bitwarden/" target="_blank">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-facebook.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;"><p style="margin-bottom: 5px; margin-top: 5px">
|
||||
© 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa
|
||||
Barbara, CA, USA
|
||||
</p>
|
||||
<p style="margin-top: 5px">
|
||||
Always confirm you are on a trusted Bitwarden domain before logging
|
||||
in:<br>
|
||||
<a href="https://bitwarden.com/" style="text-decoration:none;color:#175ddc; font-weight:400">bitwarden.com</a> |
|
||||
<a href="https://bitwarden.com/help/emails-from-bitwarden/" style="text-decoration:none; color:#175ddc; font-weight:400">Learn why we include this</a>
|
||||
</p></div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
The following emergency contacts have been removed from your account:
|
||||
|
||||
{{#each RemovedGranteeNames}}
|
||||
{{this}}
|
||||
{{/each}}
|
||||
|
||||
Learn more about emergency access at {{EmergencyAccessHelpPageUrl}}
|
||||
@@ -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);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Subscriptions.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
@@ -29,6 +30,7 @@ public interface IUpdatePremiumStorageCommand
|
||||
}
|
||||
|
||||
public class UpdatePremiumStorageCommand(
|
||||
IBraintreeService braintreeService,
|
||||
IStripeAdapter stripeAdapter,
|
||||
IUserService userService,
|
||||
IPricingClient pricingClient,
|
||||
@@ -49,7 +51,10 @@ public class UpdatePremiumStorageCommand(
|
||||
|
||||
// Fetch all premium plans and the user's subscription to find which plan they're on
|
||||
var premiumPlans = await pricingClient.ListPremiumPlans();
|
||||
var subscription = await stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId);
|
||||
var subscription = await stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, new SubscriptionGetOptions
|
||||
{
|
||||
Expand = ["customer"]
|
||||
});
|
||||
|
||||
// Find the password manager subscription item (seat, not storage) and match it to a plan
|
||||
var passwordManagerItem = subscription.Items.Data.FirstOrDefault(i =>
|
||||
@@ -127,13 +132,41 @@ public class UpdatePremiumStorageCommand(
|
||||
});
|
||||
}
|
||||
|
||||
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items = subscriptionItemOptions,
|
||||
ProrationBehavior = ProrationBehavior.AlwaysInvoice
|
||||
};
|
||||
var usingPayPal = subscription.Customer.Metadata.ContainsKey(MetadataKeys.BraintreeCustomerId);
|
||||
|
||||
await stripeAdapter.UpdateSubscriptionAsync(subscription.Id, subscriptionUpdateOptions);
|
||||
if (usingPayPal)
|
||||
{
|
||||
var options = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items = subscriptionItemOptions,
|
||||
ProrationBehavior = ProrationBehavior.CreateProrations
|
||||
};
|
||||
|
||||
await stripeAdapter.UpdateSubscriptionAsync(subscription.Id, options);
|
||||
|
||||
var draftInvoice = await stripeAdapter.CreateInvoiceAsync(new InvoiceCreateOptions
|
||||
{
|
||||
Customer = subscription.CustomerId,
|
||||
Subscription = subscription.Id,
|
||||
AutoAdvance = false,
|
||||
CollectionMethod = CollectionMethod.ChargeAutomatically
|
||||
});
|
||||
|
||||
var finalizedInvoice = await stripeAdapter.FinalizeInvoiceAsync(draftInvoice.Id,
|
||||
new InvoiceFinalizeOptions { AutoAdvance = false, Expand = ["customer"] });
|
||||
|
||||
await braintreeService.PayInvoice(new UserId(user.Id), finalizedInvoice);
|
||||
}
|
||||
else
|
||||
{
|
||||
var options = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items = subscriptionItemOptions,
|
||||
ProrationBehavior = ProrationBehavior.AlwaysInvoice
|
||||
};
|
||||
|
||||
await stripeAdapter.UpdateSubscriptionAsync(subscription.Id, options);
|
||||
}
|
||||
|
||||
// Update the user's max storage
|
||||
user.MaxStorageGb = maxStorageGb;
|
||||
|
||||
@@ -24,6 +24,7 @@ public interface IStripeAdapter
|
||||
Task<Subscription> CancelSubscriptionAsync(string id, SubscriptionCancelOptions options = null);
|
||||
Task<Invoice> GetInvoiceAsync(string id, InvoiceGetOptions options);
|
||||
Task<List<Invoice>> ListInvoicesAsync(StripeInvoiceListOptions options);
|
||||
Task<Invoice> CreateInvoiceAsync(InvoiceCreateOptions options);
|
||||
Task<Invoice> CreateInvoicePreviewAsync(InvoiceCreatePreviewOptions options);
|
||||
Task<List<Invoice>> SearchInvoiceAsync(InvoiceSearchOptions options);
|
||||
Task<Invoice> UpdateInvoiceAsync(string id, InvoiceUpdateOptions options);
|
||||
|
||||
@@ -116,6 +116,9 @@ public class StripeAdapter : IStripeAdapter
|
||||
return invoices;
|
||||
}
|
||||
|
||||
public Task<Invoice> CreateInvoiceAsync(InvoiceCreateOptions options) =>
|
||||
_invoiceService.CreateAsync(options);
|
||||
|
||||
public Task<Invoice> CreateInvoicePreviewAsync(InvoiceCreatePreviewOptions options) =>
|
||||
_invoiceService.CreatePreviewAsync(options);
|
||||
|
||||
|
||||
@@ -141,13 +141,15 @@ 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";
|
||||
|
||||
/* Architecture */
|
||||
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";
|
||||
@@ -159,11 +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 RedirectOnSsoRequired = "pm-1632-redirect-on-sso-required";
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-include path="../../../../components/head.mjml" />
|
||||
</mj-head>
|
||||
<mj-body css-class="border-fix">
|
||||
<!-- Blue Header Section -->
|
||||
<mj-wrapper css-class="border-fix" padding="20px 20px 0px 20px">
|
||||
<mj-bw-hero title=""/>
|
||||
</mj-wrapper>
|
||||
|
||||
<!-- Main Content -->
|
||||
<mj-wrapper padding="0px 20px 0px 20px">
|
||||
<mj-section background-color="#fff" padding="0px 10px 0px 10px">
|
||||
<mj-column>
|
||||
<mj-text font-size="16px" line-height="24px" padding="10px 15px">
|
||||
The following emergency contacts have been removed from your account:
|
||||
<ul>
|
||||
{{#each RemovedGranteeNames}}
|
||||
<li>{{this}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
Learn more about <a href="{{EmergencyAccessHelpPageUrl}}">emergency access</a>.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-wrapper>
|
||||
|
||||
<!-- Footer -->
|
||||
<mj-include path="../../../../components/footer.mjml" />
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -18,7 +18,7 @@
|
||||
at {{BaseAnnualRenewalPrice}} + tax.
|
||||
</mj-text>
|
||||
<mj-text font-size="16px" line-height="24px" padding="10px 15px 15px 15px">
|
||||
As a long time Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this renewal.
|
||||
As a long time Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this year's renewal.
|
||||
This renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.
|
||||
</mj-text>
|
||||
<mj-text font-size="16px" line-height="24px" padding="10px 15px">
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
Your Bitwarden Premium subscription renews in 15 days. The price is updating to {{BaseMonthlyRenewalPrice}}/month, billed annually.
|
||||
</mj-text>
|
||||
<mj-text font-size="16px" line-height="24px" padding="10px 15px 15px 15px">
|
||||
As an existing Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this renewal.
|
||||
This renewal now will be {{DiscountedMonthlyRenewalPrice}}/month, billed annually.
|
||||
As an existing Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this year's renewal.
|
||||
This renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.
|
||||
</mj-text>
|
||||
<mj-text font-size="16px" line-height="24px" padding="10px 15px">
|
||||
Questions? Contact
|
||||
|
||||
@@ -202,7 +202,7 @@
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 15px 15px 15px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">As a long time Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this renewal.
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">As a long time Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this year's renewal.
|
||||
This renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.</div>
|
||||
|
||||
</td>
|
||||
@@ -271,12 +271,12 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px 20px 10px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#f6f6f6" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#F3F6F9" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="background:#f6f6f6;background-color:#f6f6f6;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;">
|
||||
<div style="background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f6f6f6;background-color:#f6f6f6;width:100%;border-radius:0px 0px 4px 4px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:5px 10px 10px 10px;text-align:center;">
|
||||
@@ -364,8 +364,8 @@
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:660px;" ><![endif]-->
|
||||
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:620px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
@@ -381,13 +381,13 @@
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://x.com/bitwarden" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-x.png" style="border-radius:3px;display:block;" width="30">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -404,13 +404,13 @@
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.reddit.com/r/Bitwarden/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-reddit.png" style="border-radius:3px;display:block;" width="30">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-reddit.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -427,13 +427,13 @@
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://community.bitwarden.com/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-discourse.png" style="border-radius:3px;display:block;" width="30">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-discourse.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -450,13 +450,13 @@
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/bitwarden" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-github.png" style="border-radius:3px;display:block;" width="30">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-github.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -473,13 +473,13 @@
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-youtube.png" style="border-radius:3px;display:block;" width="30">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-youtube.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -496,13 +496,13 @@
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.linkedin.com/company/bitwarden1/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-linkedin.png" style="border-radius:3px;display:block;" width="30">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-linkedin.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -519,13 +519,13 @@
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.facebook.com/bitwarden/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-facebook.png" style="border-radius:3px;display:block;" width="30">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-facebook.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -546,15 +546,15 @@
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;"><p style="margin-bottom: 5px">
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;"><p style="margin-bottom: 5px; margin-top: 5px">
|
||||
© 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa
|
||||
Barbara, CA, USA
|
||||
</p>
|
||||
<p style="margin-top: 5px">
|
||||
Always confirm you are on a trusted Bitwarden domain before logging
|
||||
in:<br>
|
||||
<a href="https://bitwarden.com/">bitwarden.com</a> |
|
||||
<a href="https://bitwarden.com/help/emails-from-bitwarden/">Learn why we include this</a>
|
||||
<a href="https://bitwarden.com/" style="text-decoration:none;color:#175ddc; font-weight:400">bitwarden.com</a> |
|
||||
<a href="https://bitwarden.com/help/emails-from-bitwarden/" style="text-decoration:none; color:#175ddc; font-weight:400">Learn why we include this</a>
|
||||
</p></div>
|
||||
|
||||
</td>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Your Bitwarden Families subscription renews in 15 days. The price is updating to {{BaseMonthlyRenewalPrice}}/month, billed annually
|
||||
at {{BaseAnnualRenewalPrice}} + tax.
|
||||
|
||||
As a long time Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this renewal.
|
||||
As a long time Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this year's renewal.
|
||||
This renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.
|
||||
|
||||
Questions? Contact support@bitwarden.com
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Bit.Core.Models.Mail.Billing.Renewal.Premium;
|
||||
public class PremiumRenewalMailView : BaseMailView
|
||||
{
|
||||
public required string BaseMonthlyRenewalPrice { get; set; }
|
||||
public required string DiscountedMonthlyRenewalPrice { get; set; }
|
||||
public required string DiscountedAnnualRenewalPrice { get; set; }
|
||||
public required string DiscountAmount { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -201,8 +201,8 @@
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 15px 15px 15px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">As an existing Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this renewal.
|
||||
This renewal now will be {{DiscountedMonthlyRenewalPrice}}/month, billed annually.</div>
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">As an existing Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this year's renewal.
|
||||
This renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
@@ -270,12 +270,12 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:0px 20px 10px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#f6f6f6" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#F3F6F9" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
|
||||
|
||||
<div style="background:#f6f6f6;background-color:#f6f6f6;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;">
|
||||
<div style="background:#F3F6F9;background-color:#F3F6F9;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;">
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f6f6f6;background-color:#f6f6f6;width:100%;border-radius:0px 0px 4px 4px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#F3F6F9;background-color:#F3F6F9;width:100%;border-radius:0px 0px 4px 4px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:5px 10px 10px 10px;text-align:center;">
|
||||
@@ -363,8 +363,8 @@
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:660px;" ><![endif]-->
|
||||
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:620px;" ><![endif]-->
|
||||
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
|
||||
@@ -380,13 +380,13 @@
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://x.com/bitwarden" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-x.png" style="border-radius:3px;display:block;" width="30">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -403,13 +403,13 @@
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.reddit.com/r/Bitwarden/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-reddit.png" style="border-radius:3px;display:block;" width="30">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-reddit.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -426,13 +426,13 @@
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://community.bitwarden.com/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-discourse.png" style="border-radius:3px;display:block;" width="30">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-discourse.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -449,13 +449,13 @@
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://github.com/bitwarden" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-github.png" style="border-radius:3px;display:block;" width="30">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-github.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -472,13 +472,13 @@
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-youtube.png" style="border-radius:3px;display:block;" width="30">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-youtube.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -495,13 +495,13 @@
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.linkedin.com/company/bitwarden1/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-linkedin.png" style="border-radius:3px;display:block;" width="30">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-linkedin.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -518,13 +518,13 @@
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td style="padding:10px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||
<td style="padding:8px;vertical-align:middle;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
|
||||
<a href="https://www.facebook.com/bitwarden/" target="_blank">
|
||||
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-facebook.png" style="border-radius:3px;display:block;" width="30">
|
||||
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-facebook.png" style="border-radius:3px;display:block;" width="24">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -545,15 +545,15 @@
|
||||
<tr>
|
||||
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;"><p style="margin-bottom: 5px">
|
||||
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;"><p style="margin-bottom: 5px; margin-top: 5px">
|
||||
© 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa
|
||||
Barbara, CA, USA
|
||||
</p>
|
||||
<p style="margin-top: 5px">
|
||||
Always confirm you are on a trusted Bitwarden domain before logging
|
||||
in:<br>
|
||||
<a href="https://bitwarden.com/">bitwarden.com</a> |
|
||||
<a href="https://bitwarden.com/help/emails-from-bitwarden/">Learn why we include this</a>
|
||||
<a href="https://bitwarden.com/" style="text-decoration:none;color:#175ddc; font-weight:400">bitwarden.com</a> |
|
||||
<a href="https://bitwarden.com/help/emails-from-bitwarden/" style="text-decoration:none; color:#175ddc; font-weight:400">Learn why we include this</a>
|
||||
</p></div>
|
||||
|
||||
</td>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Your Bitwarden Premium subscription renews in 15 days. The price is updating to {{BaseMonthlyRenewalPrice}}/month, billed annually.
|
||||
|
||||
As an existing Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this renewal.
|
||||
This renewal now will be {{DiscountedMonthlyRenewalPrice}}/month, billed annually.
|
||||
As an existing Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this year's renewal.
|
||||
This renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.
|
||||
|
||||
Questions? Contact support@bitwarden.com
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -657,11 +657,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.");
|
||||
}
|
||||
|
||||
@@ -83,7 +83,6 @@ public class GlobalSettings : IGlobalSettings
|
||||
public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings();
|
||||
public virtual string DevelopmentDirectory { get; set; }
|
||||
public virtual IWebPushSettings WebPush { get; set; } = new WebPushSettings();
|
||||
|
||||
public virtual int SendAccessTokenLifetimeInMinutes { get; set; } = 5;
|
||||
public virtual bool EnableEmailVerification { get; set; }
|
||||
public virtual string KdfDefaultHashKey { get; set; }
|
||||
@@ -93,6 +92,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
public virtual string SendDefaultHashKey { get; set; }
|
||||
public virtual string PricingUri { get; set; }
|
||||
public virtual Fido2Settings Fido2 { get; set; } = new Fido2Settings();
|
||||
public virtual ICommunicationSettings Communication { get; set; } = new CommunicationSettings();
|
||||
|
||||
public string BuildExternalUri(string explicitValue, string name)
|
||||
{
|
||||
@@ -776,4 +776,17 @@ public class GlobalSettings : IGlobalSettings
|
||||
{
|
||||
public HashSet<string> Origins { get; set; }
|
||||
}
|
||||
|
||||
public class CommunicationSettings : ICommunicationSettings
|
||||
{
|
||||
public string Bootstrap { get; set; } = "none";
|
||||
public ISsoCookieVendorSettings SsoCookieVendor { get; set; } = new SsoCookieVendorSettings();
|
||||
}
|
||||
|
||||
public class SsoCookieVendorSettings : ISsoCookieVendorSettings
|
||||
{
|
||||
public string IdpLoginUrl { get; set; }
|
||||
public string CookieName { get; set; }
|
||||
public string CookieDomain { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
7
src/Core/Settings/ICommunicationSettings.cs
Normal file
7
src/Core/Settings/ICommunicationSettings.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Settings;
|
||||
|
||||
public interface ICommunicationSettings
|
||||
{
|
||||
string Bootstrap { get; set; }
|
||||
ISsoCookieVendorSettings SsoCookieVendor { get; set; }
|
||||
}
|
||||
@@ -29,4 +29,5 @@ public interface IGlobalSettings
|
||||
IWebPushSettings WebPush { get; set; }
|
||||
GlobalSettings.EventLoggingSettings EventLogging { get; set; }
|
||||
GlobalSettings.WebAuthnSettings WebAuthn { get; set; }
|
||||
ICommunicationSettings Communication { get; set; }
|
||||
}
|
||||
|
||||
8
src/Core/Settings/ISsoCookieVendorSettings.cs
Normal file
8
src/Core/Settings/ISsoCookieVendorSettings.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Settings;
|
||||
|
||||
public interface ISsoCookieVendorSettings
|
||||
{
|
||||
string IdpLoginUrl { get; set; }
|
||||
string CookieName { get; set; }
|
||||
string CookieDomain { get; set; }
|
||||
}
|
||||
@@ -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"/>.
|
||||
|
||||
@@ -74,7 +74,7 @@ public class ImportCiphersCommand : IImportCiphersCommand
|
||||
|
||||
if (cipher.UserId.HasValue && cipher.Favorite)
|
||||
{
|
||||
cipher.Favorites = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":\"true\"}}";
|
||||
cipher.Favorites = $"{{\"{cipher.UserId.ToString().ToUpperInvariant()}\":true}}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -27,6 +28,7 @@ public class SendValidationService : ISendValidationService
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
|
||||
|
||||
@@ -38,7 +40,7 @@ public class SendValidationService : ISendValidationService
|
||||
IUserService userService,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
GlobalSettings globalSettings,
|
||||
|
||||
IPricingClient pricingClient,
|
||||
ICurrentContext currentContext)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
@@ -48,6 +50,7 @@ public class SendValidationService : ISendValidationService
|
||||
_userService = userService;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_globalSettings = globalSettings;
|
||||
_pricingClient = pricingClient;
|
||||
_currentContext = currentContext;
|
||||
}
|
||||
|
||||
@@ -123,10 +126,19 @@ public class SendValidationService : ISendValidationService
|
||||
}
|
||||
else
|
||||
{
|
||||
// Users that get access to file storage/premium from their organization get the default
|
||||
// 1 GB max storage.
|
||||
short limit = _globalSettings.SelfHosted ? Constants.SelfHostedMaxStorageGb : (short)1;
|
||||
storageBytesRemaining = user.StorageBytesRemaining(limit);
|
||||
// Users that get access to file storage/premium from their organization get storage
|
||||
// based on the current premium plan from the pricing service
|
||||
short provided;
|
||||
if (_globalSettings.SelfHosted)
|
||||
{
|
||||
provided = Constants.SelfHostedMaxStorageGb;
|
||||
}
|
||||
else
|
||||
{
|
||||
var premiumPlan = await _pricingClient.GetAvailablePremiumPlan();
|
||||
provided = (short)premiumPlan.Storage.Provided;
|
||||
}
|
||||
storageBytesRemaining = user.StorageBytesRemaining(provided);
|
||||
}
|
||||
}
|
||||
else if (send.OrganizationId.HasValue)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user