mirror of
https://github.com/bitwarden/server
synced 2026-01-26 06:13:31 +00:00
Merge branch 'main' into ac/pm-23768/server-public-api---add-restore/revoke-for-members
This commit is contained in:
@@ -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"
|
||||
|
||||
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",
|
||||
|
||||
10
.github/workflows/build.yml
vendored
10
.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"
|
||||
@@ -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
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ public class RestoreOrganizationUserCommand(
|
||||
.twoFactorIsEnabled;
|
||||
}
|
||||
|
||||
if (organization.PlanType == PlanType.Free)
|
||||
if (organization.PlanType == PlanType.Free && organizationUser.UserId.HasValue)
|
||||
{
|
||||
await CheckUserForOtherFreeOrganizationOwnershipAsync(organizationUser);
|
||||
}
|
||||
|
||||
@@ -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}}
|
||||
@@ -143,6 +143,7 @@ public static class FeatureFlagKeys
|
||||
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 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";
|
||||
@@ -173,6 +174,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";
|
||||
@@ -254,6 +256,7 @@ 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";
|
||||
|
||||
/* UIF Team */
|
||||
public const string RouterFocusManagement = "router-focus-management";
|
||||
|
||||
@@ -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,8 +18,8 @@
|
||||
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.
|
||||
This year's renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.
|
||||
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">
|
||||
Questions? Contact
|
||||
|
||||
@@ -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 year's 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,8 +202,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 a long time Bitwarden customer, you will receive a one-time {{DiscountAmount}} loyalty discount for this renewal.
|
||||
This year's renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.</div>
|
||||
<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>
|
||||
</tr>
|
||||
|
||||
@@ -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.
|
||||
This year's renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.
|
||||
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 year's 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>
|
||||
|
||||
@@ -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 year's 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
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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}}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
64
src/Core/Utilities/DomainNameAttribute.cs
Normal file
64
src/Core/Utilities/DomainNameAttribute.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Bit.Core.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// https://bitwarden.atlassian.net/browse/VULN-376
|
||||
/// Domain names are vulnerable to XSS attacks if not properly validated.
|
||||
/// Domain names can contain letters, numbers, dots, and hyphens.
|
||||
/// Domain names maybe internationalized (IDN) and contain unicode characters.
|
||||
/// </summary>
|
||||
public class DomainNameValidatorAttribute : ValidationAttribute
|
||||
{
|
||||
// RFC 1123 compliant domain name regex
|
||||
// - Allows alphanumeric characters and hyphens
|
||||
// - Cannot start or end with a hyphen
|
||||
// - Each label (part between dots) must be 1-63 characters
|
||||
// - Total length should not exceed 253 characters
|
||||
// - Supports internationalized domain names (IDN) - which is why this regex includes unicode ranges
|
||||
private static readonly Regex _domainNameRegex = new(
|
||||
@"^(?:[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF](?:[a-zA-Z0-9\-\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{0,61}[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?\.)*[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF](?:[a-zA-Z0-9\-\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{0,61}[a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?$",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase
|
||||
);
|
||||
|
||||
public DomainNameValidatorAttribute()
|
||||
: base("The {0} field is not a valid domain name.")
|
||||
{ }
|
||||
|
||||
public override bool IsValid(object? value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return true; // Use [Required] for null checks
|
||||
}
|
||||
|
||||
var domainName = value.ToString();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(domainName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject if contains any whitespace (including leading/trailing spaces, tabs, newlines)
|
||||
if (domainName.Any(char.IsWhiteSpace))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check length constraints
|
||||
if (domainName.Length > 253)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for control characters or other dangerous characters
|
||||
if (domainName.Any(c => char.IsControl(c) || c == '<' || c == '>' || c == '"' || c == '\'' || c == '&'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate against domain name regex
|
||||
return _domainNameRegex.IsMatch(domainName);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Platform.Push;
|
||||
@@ -46,6 +47,7 @@ public class CipherService : ICipherService
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public CipherService(
|
||||
ICipherRepository cipherRepository,
|
||||
@@ -65,7 +67,8 @@ public class CipherService : ICipherService
|
||||
IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IFeatureService featureService)
|
||||
IFeatureService featureService,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_cipherRepository = cipherRepository;
|
||||
_folderRepository = folderRepository;
|
||||
@@ -85,6 +88,7 @@ public class CipherService : ICipherService
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_featureService = featureService;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
|
||||
@@ -943,10 +947,19 @@ public class CipherService : ICipherService
|
||||
}
|
||||
else
|
||||
{
|
||||
// Users that get access to file storage/premium from their organization get the default
|
||||
// 1 GB max storage.
|
||||
storageBytesRemaining = user.StorageBytesRemaining(
|
||||
_globalSettings.SelfHosted ? Constants.SelfHostedMaxStorageGb : (short)1);
|
||||
// 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 (cipher.OrganizationId.HasValue)
|
||||
|
||||
@@ -22,6 +22,7 @@ using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Auth.Services.Implementations;
|
||||
using Bit.Core.Auth.UserFeatures;
|
||||
using Bit.Core.Auth.UserFeatures.EmergencyAccess;
|
||||
using Bit.Core.Auth.UserFeatures.PasswordValidation;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Implementations;
|
||||
|
||||
@@ -6,6 +6,7 @@ using Bit.Api.Models.Public.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Platform.Push;
|
||||
@@ -114,4 +115,64 @@ public class CollectionsControllerTests : IClassFixture<ApiApplicationFactory>,
|
||||
Assert.NotEmpty(result.Item2.Groups);
|
||||
Assert.NotEmpty(result.Item2.Users);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_ExcludesDefaultUserCollections_IncludesGroupsAndUsers()
|
||||
{
|
||||
// Arrange
|
||||
var collectionRepository = _factory.GetService<ICollectionRepository>();
|
||||
var groupRepository = _factory.GetService<IGroupRepository>();
|
||||
|
||||
var defaultCollection = new Collection
|
||||
{
|
||||
OrganizationId = _organization.Id,
|
||||
Name = "My Items",
|
||||
Type = CollectionType.DefaultUserCollection
|
||||
};
|
||||
await collectionRepository.CreateAsync(defaultCollection, null, null);
|
||||
|
||||
var group = await groupRepository.CreateAsync(new Group
|
||||
{
|
||||
OrganizationId = _organization.Id,
|
||||
Name = "Test Group",
|
||||
ExternalId = $"test-group-{Guid.NewGuid()}",
|
||||
});
|
||||
|
||||
var (_, user) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(
|
||||
_factory,
|
||||
_organization.Id,
|
||||
OrganizationUserType.User);
|
||||
|
||||
var sharedCollection = await OrganizationTestHelpers.CreateCollectionAsync(
|
||||
_factory,
|
||||
_organization.Id,
|
||||
"Shared Collection with Access",
|
||||
externalId: "shared-collection-with-access",
|
||||
groups:
|
||||
[
|
||||
new CollectionAccessSelection { Id = group.Id, ReadOnly = false, HidePasswords = false, Manage = true }
|
||||
],
|
||||
users:
|
||||
[
|
||||
new CollectionAccessSelection { Id = user.Id, ReadOnly = true, HidePasswords = true, Manage = false }
|
||||
]);
|
||||
|
||||
// Act
|
||||
var response = await _client.GetFromJsonAsync<ListResponseModel<CollectionResponseModel>>("public/collections");
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(response);
|
||||
|
||||
Assert.DoesNotContain(response.Data, c => c.Id == defaultCollection.Id);
|
||||
|
||||
var collectionResponse = response.Data.First(c => c.Id == sharedCollection.Id);
|
||||
Assert.NotNull(collectionResponse.Groups);
|
||||
Assert.Single(collectionResponse.Groups);
|
||||
|
||||
var groupResponse = collectionResponse.Groups.First();
|
||||
Assert.Equal(group.Id, groupResponse.Id);
|
||||
Assert.False(groupResponse.ReadOnly);
|
||||
Assert.False(groupResponse.HidePasswords);
|
||||
Assert.True(groupResponse.Manage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
email.ToEmails.Contains("user@example.com") &&
|
||||
email.Subject == "Your Bitwarden Premium renewal is updating" &&
|
||||
email.View.BaseMonthlyRenewalPrice == (plan.Seat.Price / 12).ToString("C", new CultureInfo("en-US")) &&
|
||||
email.View.DiscountedMonthlyRenewalPrice == (discountedPrice / 12).ToString("C", new CultureInfo("en-US")) &&
|
||||
email.View.DiscountedAnnualRenewalPrice == discountedPrice.ToString("C", new CultureInfo("en-US")) &&
|
||||
email.View.DiscountAmount == $"{coupon.PercentOff}%"
|
||||
));
|
||||
}
|
||||
@@ -2436,7 +2436,7 @@ public class UpcomingInvoiceHandlerTests
|
||||
email.Subject == "Your Bitwarden Premium renewal is updating" &&
|
||||
email.View.BaseMonthlyRenewalPrice == (plan.Seat.Price / 12).ToString("C", new CultureInfo("en-US")) &&
|
||||
email.View.DiscountAmount == "30%" &&
|
||||
email.View.DiscountedMonthlyRenewalPrice == (expectedDiscountedPrice / 12).ToString("C", new CultureInfo("en-US"))
|
||||
email.View.DiscountedAnnualRenewalPrice == expectedDiscountedPrice.ToString("C", new CultureInfo("en-US"))
|
||||
));
|
||||
|
||||
await _mailService.DidNotReceive().SendInvoiceUpcoming(
|
||||
|
||||
@@ -715,6 +715,39 @@ public class RestoreOrganizationUserCommandTests
|
||||
Arg.Is<OrganizationUserStatusType>(x => x != OrganizationUserStatusType.Revoked));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUser_InvitedUserInFreeOrganization_Success(
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||
{
|
||||
organization.PlanType = PlanType.Free;
|
||||
organizationUser.UserId = null;
|
||||
organizationUser.Key = null;
|
||||
organizationUser.Status = OrganizationUserStatusType.Revoked;
|
||||
|
||||
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id).Returns(new OrganizationSeatCounts
|
||||
{
|
||||
Sponsored = 0,
|
||||
Users = 1
|
||||
});
|
||||
|
||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.DidNotReceiveWithAnyArgs()
|
||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RestoreUsers_Success(Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
using Bit.Core.Auth.UserFeatures.EmergencyAccess.Mail;
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Platform.Mail.Delivery;
|
||||
using Bit.Core.Platform.Mail.Mailer;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||
|
||||
namespace Bit.Core.Test.Auth.UserFeatures.EmergencyAccess;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class EmergencyAccessMailTests
|
||||
{
|
||||
// Constant values for all Emergency Access emails
|
||||
private const string _emergencyAccessHelpUrl = "https://bitwarden.com/help/emergency-access/";
|
||||
private const string _emergencyAccessMailSubject = "Emergency contacts removed";
|
||||
|
||||
/// <summary>
|
||||
/// Documents how to construct and send the emergency access removal email.
|
||||
/// 1. Inject IMailer into their command/service
|
||||
/// 2. Construct EmergencyAccessRemoveGranteesMail as shown below
|
||||
/// 3. Call mailer.SendEmail(mail)
|
||||
/// </summary>
|
||||
[Theory, BitAutoData]
|
||||
public async Task SendEmergencyAccessRemoveGranteesEmail_SingleGrantee_Success(
|
||||
string grantorEmail,
|
||||
string granteeName)
|
||||
{
|
||||
// Arrange
|
||||
var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
var deliveryService = Substitute.For<IMailDeliveryService>();
|
||||
var mailer = new Mailer(
|
||||
new HandlebarMailRenderer(logger, globalSettings),
|
||||
deliveryService);
|
||||
|
||||
var mail = new EmergencyAccessRemoveGranteesMail
|
||||
{
|
||||
ToEmails = [grantorEmail],
|
||||
View = new EmergencyAccessRemoveGranteesMailView
|
||||
{
|
||||
RemovedGranteeNames = [granteeName]
|
||||
}
|
||||
};
|
||||
|
||||
MailMessage sentMessage = null;
|
||||
await deliveryService.SendEmailAsync(Arg.Do<MailMessage>(message =>
|
||||
sentMessage = message
|
||||
));
|
||||
|
||||
// Act
|
||||
await mailer.SendEmail(mail);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(sentMessage);
|
||||
Assert.Contains(grantorEmail, sentMessage.ToEmails);
|
||||
|
||||
// Verify the content contains the grantee name
|
||||
Assert.Contains(granteeName, sentMessage.TextContent);
|
||||
Assert.Contains(granteeName, sentMessage.HtmlContent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Documents handling multiple removed grantees in a single email.
|
||||
/// </summary>
|
||||
[Theory, BitAutoData]
|
||||
public async Task SendEmergencyAccessRemoveGranteesEmail_MultipleGrantees_RendersAllNames(
|
||||
string grantorEmail)
|
||||
{
|
||||
// Arrange
|
||||
var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();
|
||||
var globalSettings = new GlobalSettings { SelfHosted = false };
|
||||
var deliveryService = Substitute.For<IMailDeliveryService>();
|
||||
var mailer = new Mailer(
|
||||
new HandlebarMailRenderer(logger, globalSettings),
|
||||
deliveryService);
|
||||
|
||||
var granteeNames = new[] { "Alice", "Bob", "Carol" };
|
||||
|
||||
var mail = new EmergencyAccessRemoveGranteesMail
|
||||
{
|
||||
ToEmails = [grantorEmail],
|
||||
View = new EmergencyAccessRemoveGranteesMailView
|
||||
{
|
||||
RemovedGranteeNames = granteeNames
|
||||
}
|
||||
};
|
||||
|
||||
MailMessage sentMessage = null;
|
||||
await deliveryService.SendEmailAsync(Arg.Do<MailMessage>(message =>
|
||||
sentMessage = message
|
||||
));
|
||||
|
||||
// Act
|
||||
await mailer.SendEmail(mail);
|
||||
|
||||
// Assert - All grantee names should appear in the email
|
||||
Assert.NotNull(sentMessage);
|
||||
foreach (var granteeName in granteeNames)
|
||||
{
|
||||
Assert.Contains(granteeName, sentMessage.TextContent);
|
||||
Assert.Contains(granteeName, sentMessage.HtmlContent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the required GranteeNames for the email view model.
|
||||
/// </summary>
|
||||
[Theory, BitAutoData]
|
||||
public void EmergencyAccessRemoveGranteesMailView_GranteeNames_AreRequired(
|
||||
string grantorEmail)
|
||||
{
|
||||
// Arrange - Shows the minimum required to construct the email
|
||||
var mail = new EmergencyAccessRemoveGranteesMail
|
||||
{
|
||||
ToEmails = [grantorEmail], // Required: who to send to
|
||||
View = new EmergencyAccessRemoveGranteesMailView
|
||||
{
|
||||
// Required: at least one removed grantee name
|
||||
RemovedGranteeNames = ["Example Grantee"]
|
||||
}
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(mail);
|
||||
Assert.NotNull(mail.View);
|
||||
Assert.NotEmpty(mail.View.RemovedGranteeNames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure consistency with help pages link and email subject.
|
||||
/// </summary>
|
||||
/// <param name="grantorEmail"></param>
|
||||
/// <param name="granteeName"></param>
|
||||
[Theory, BitAutoData]
|
||||
public void EmergencyAccessRemoveGranteesMailView_SubjectAndHelpLink_MatchesExpectedValues(string grantorEmail, string granteeName)
|
||||
{
|
||||
// Arrange
|
||||
var mail = new EmergencyAccessRemoveGranteesMail
|
||||
{
|
||||
ToEmails = [grantorEmail],
|
||||
View = new EmergencyAccessRemoveGranteesMailView { RemovedGranteeNames = [granteeName] }
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(mail);
|
||||
Assert.NotNull(mail.View);
|
||||
Assert.Equal(_emergencyAccessMailSubject, mail.Subject);
|
||||
Assert.Equal(_emergencyAccessHelpUrl, mail.View.EmergencyAccessHelpPageUrl);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
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;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Auth.UserFeatures.EmergencyAccess;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@@ -17,7 +16,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Auth.Services;
|
||||
namespace Bit.Core.Test.Auth.UserFeatures.EmergencyAccess;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class EmergencyAccessServiceTests
|
||||
@@ -68,13 +67,13 @@ public class EmergencyAccessServiceTests
|
||||
Assert.Equal(EmergencyAccessStatusType.Invited, result.Status);
|
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync(Arg.Any<EmergencyAccess>());
|
||||
.CreateAsync(Arg.Any<Core.Auth.Entities.EmergencyAccess>());
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()
|
||||
.Received(1)
|
||||
.Protect(Arg.Any<EmergencyAccessInviteTokenable>());
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendEmergencyAccessInviteEmailAsync(Arg.Any<EmergencyAccess>(), Arg.Any<string>(), Arg.Any<string>());
|
||||
.SendEmergencyAccessInviteEmailAsync(Arg.Any<Core.Auth.Entities.EmergencyAccess>(), Arg.Any<string>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -98,7 +97,7 @@ public class EmergencyAccessServiceTests
|
||||
User invitingUser,
|
||||
Guid emergencyAccessId)
|
||||
{
|
||||
EmergencyAccess emergencyAccess = null;
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess = null;
|
||||
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
@@ -119,7 +118,7 @@ public class EmergencyAccessServiceTests
|
||||
User invitingUser,
|
||||
Guid emergencyAccessId)
|
||||
{
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
|
||||
{
|
||||
Status = EmergencyAccessStatusType.Invited,
|
||||
GrantorId = Guid.NewGuid(),
|
||||
@@ -148,7 +147,7 @@ public class EmergencyAccessServiceTests
|
||||
User invitingUser,
|
||||
Guid emergencyAccessId)
|
||||
{
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
|
||||
{
|
||||
Status = statusType,
|
||||
GrantorId = invitingUser.Id,
|
||||
@@ -172,7 +171,7 @@ public class EmergencyAccessServiceTests
|
||||
User invitingUser,
|
||||
Guid emergencyAccessId)
|
||||
{
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
|
||||
{
|
||||
Status = EmergencyAccessStatusType.Invited,
|
||||
GrantorId = invitingUser.Id,
|
||||
@@ -194,7 +193,7 @@ public class EmergencyAccessServiceTests
|
||||
public async Task AcceptUserAsync_EmergencyAccessNull_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider, User acceptingUser, string token)
|
||||
{
|
||||
EmergencyAccess emergencyAccess = null;
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess = null;
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(emergencyAccess);
|
||||
@@ -209,7 +208,7 @@ public class EmergencyAccessServiceTests
|
||||
public async Task AcceptUserAsync_CannotUnprotectToken_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User acceptingUser,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
string token)
|
||||
{
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
@@ -230,8 +229,8 @@ public class EmergencyAccessServiceTests
|
||||
public async Task AcceptUserAsync_TokenDataInvalid_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User acceptingUser,
|
||||
EmergencyAccess emergencyAccess,
|
||||
EmergencyAccess wrongEmergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess wrongEmergencyAccess,
|
||||
string token)
|
||||
{
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
@@ -257,7 +256,7 @@ public class EmergencyAccessServiceTests
|
||||
public async Task AcceptUserAsync_AcceptedStatus_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User acceptingUser,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
string token)
|
||||
{
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.Accepted;
|
||||
@@ -284,7 +283,7 @@ public class EmergencyAccessServiceTests
|
||||
public async Task AcceptUserAsync_NotInvitedStatus_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User acceptingUser,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
string token)
|
||||
{
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.Confirmed;
|
||||
@@ -311,7 +310,7 @@ public class EmergencyAccessServiceTests
|
||||
public async Task AcceptUserAsync_EmergencyAccessEmailDoesNotMatch_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User acceptingUser,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
string token)
|
||||
{
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.Invited;
|
||||
@@ -339,7 +338,7 @@ public class EmergencyAccessServiceTests
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User acceptingUser,
|
||||
User invitingUser,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
string token)
|
||||
{
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.Invited;
|
||||
@@ -364,7 +363,7 @@ public class EmergencyAccessServiceTests
|
||||
|
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.Received(1)
|
||||
.ReplaceAsync(Arg.Is<EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Accepted));
|
||||
.ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Accepted));
|
||||
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
@@ -375,11 +374,11 @@ public class EmergencyAccessServiceTests
|
||||
public async Task DeleteAsync_EmergencyAccessNull_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User invitingUser,
|
||||
EmergencyAccess emergencyAccess)
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess)
|
||||
{
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns((EmergencyAccess)null);
|
||||
.Returns((Core.Auth.Entities.EmergencyAccess)null);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.DeleteAsync(emergencyAccess.Id, invitingUser.Id));
|
||||
@@ -391,7 +390,7 @@ public class EmergencyAccessServiceTests
|
||||
public async Task DeleteAsync_EmergencyAccessGrantorIdNotEqual_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User invitingUser,
|
||||
EmergencyAccess emergencyAccess)
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess)
|
||||
{
|
||||
emergencyAccess.GrantorId = Guid.NewGuid();
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
@@ -408,7 +407,7 @@ public class EmergencyAccessServiceTests
|
||||
public async Task DeleteAsync_EmergencyAccessGranteeIdNotEqual_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User invitingUser,
|
||||
EmergencyAccess emergencyAccess)
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess)
|
||||
{
|
||||
emergencyAccess.GranteeId = Guid.NewGuid();
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
@@ -425,7 +424,7 @@ public class EmergencyAccessServiceTests
|
||||
public async Task DeleteAsync_EmergencyAccessIsDeleted_Success(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
User user,
|
||||
EmergencyAccess emergencyAccess)
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess)
|
||||
{
|
||||
emergencyAccess.GranteeId = user.Id;
|
||||
emergencyAccess.GrantorId = user.Id;
|
||||
@@ -443,7 +442,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_EmergencyAccessNull_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
string key,
|
||||
User grantorUser)
|
||||
{
|
||||
@@ -451,7 +450,7 @@ public class EmergencyAccessServiceTests
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated;
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns((EmergencyAccess)null);
|
||||
.Returns((Core.Auth.Entities.EmergencyAccess)null);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id));
|
||||
@@ -463,7 +462,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_EmergencyAccessStatusIsNotAccepted_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
string key,
|
||||
User grantorUser)
|
||||
{
|
||||
@@ -484,7 +483,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_EmergencyAccessGrantorIdNotEqualToConfirmingUserId_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
string key,
|
||||
User grantorUser)
|
||||
{
|
||||
@@ -505,7 +504,7 @@ public class EmergencyAccessServiceTests
|
||||
SutProvider<EmergencyAccessService> sutProvider, User confirmingUser, string key)
|
||||
{
|
||||
confirmingUser.UsesKeyConnector = true;
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
|
||||
{
|
||||
Status = EmergencyAccessStatusType.Accepted,
|
||||
GrantorId = confirmingUser.Id,
|
||||
@@ -530,7 +529,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task ConfirmUserAsync_ConfirmsAndReplacesEmergencyAccess_Success(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
string key,
|
||||
User grantorUser,
|
||||
User granteeUser)
|
||||
@@ -553,7 +552,7 @@ public class EmergencyAccessServiceTests
|
||||
|
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.Received(1)
|
||||
.ReplaceAsync(Arg.Is<EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Confirmed));
|
||||
.ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Confirmed));
|
||||
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
@@ -564,7 +563,7 @@ public class EmergencyAccessServiceTests
|
||||
public async Task SaveAsync_PremiumCannotUpdate_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider, User savingUser)
|
||||
{
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
|
||||
{
|
||||
Type = EmergencyAccessType.Takeover,
|
||||
GrantorId = savingUser.Id,
|
||||
@@ -586,7 +585,7 @@ public class EmergencyAccessServiceTests
|
||||
SutProvider<EmergencyAccessService> sutProvider, User savingUser)
|
||||
{
|
||||
savingUser.Premium = true;
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
|
||||
{
|
||||
Type = EmergencyAccessType.Takeover,
|
||||
GrantorId = new Guid(),
|
||||
@@ -611,7 +610,7 @@ public class EmergencyAccessServiceTests
|
||||
SutProvider<EmergencyAccessService> sutProvider, User grantorUser)
|
||||
{
|
||||
grantorUser.UsesKeyConnector = true;
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
|
||||
{
|
||||
Type = EmergencyAccessType.Takeover,
|
||||
GrantorId = grantorUser.Id,
|
||||
@@ -633,7 +632,7 @@ public class EmergencyAccessServiceTests
|
||||
SutProvider<EmergencyAccessService> sutProvider, User grantorUser)
|
||||
{
|
||||
grantorUser.UsesKeyConnector = true;
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
|
||||
{
|
||||
Type = EmergencyAccessType.View,
|
||||
GrantorId = grantorUser.Id,
|
||||
@@ -655,7 +654,7 @@ public class EmergencyAccessServiceTests
|
||||
SutProvider<EmergencyAccessService> sutProvider, User grantorUser)
|
||||
{
|
||||
grantorUser.UsesKeyConnector = false;
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
|
||||
{
|
||||
Type = EmergencyAccessType.Takeover,
|
||||
GrantorId = grantorUser.Id,
|
||||
@@ -678,7 +677,7 @@ public class EmergencyAccessServiceTests
|
||||
{
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns((EmergencyAccess)null);
|
||||
.Returns((Core.Auth.Entities.EmergencyAccess)null);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser));
|
||||
@@ -692,7 +691,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task InitiateAsync_EmergencyAccessGranteeIdNotEqual_ThrowBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User initiatingUser)
|
||||
{
|
||||
emergencyAccess.GranteeId = new Guid();
|
||||
@@ -712,7 +711,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task InitiateAsync_EmergencyAccessStatusIsNotConfirmed_ThrowBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User initiatingUser)
|
||||
{
|
||||
emergencyAccess.GranteeId = initiatingUser.Id;
|
||||
@@ -735,7 +734,7 @@ public class EmergencyAccessServiceTests
|
||||
SutProvider<EmergencyAccessService> sutProvider, User initiatingUser, User grantor)
|
||||
{
|
||||
grantor.UsesKeyConnector = true;
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
|
||||
{
|
||||
Status = EmergencyAccessStatusType.Confirmed,
|
||||
GranteeId = initiatingUser.Id,
|
||||
@@ -764,7 +763,7 @@ public class EmergencyAccessServiceTests
|
||||
SutProvider<EmergencyAccessService> sutProvider, User initiatingUser, User grantor)
|
||||
{
|
||||
grantor.UsesKeyConnector = true;
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
|
||||
{
|
||||
Status = EmergencyAccessStatusType.Confirmed,
|
||||
GranteeId = initiatingUser.Id,
|
||||
@@ -783,14 +782,14 @@ public class EmergencyAccessServiceTests
|
||||
|
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.Received(1)
|
||||
.ReplaceAsync(Arg.Is<EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.RecoveryInitiated));
|
||||
.ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.RecoveryInitiated));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task InitiateAsync_RequestIsCorrect_Success(
|
||||
SutProvider<EmergencyAccessService> sutProvider, User initiatingUser, User grantor)
|
||||
{
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
|
||||
{
|
||||
Status = EmergencyAccessStatusType.Confirmed,
|
||||
GranteeId = initiatingUser.Id,
|
||||
@@ -809,7 +808,7 @@ public class EmergencyAccessServiceTests
|
||||
|
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.Received(1)
|
||||
.ReplaceAsync(Arg.Is<EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.RecoveryInitiated));
|
||||
.ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.RecoveryInitiated));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -818,7 +817,7 @@ public class EmergencyAccessServiceTests
|
||||
{
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns((EmergencyAccess)null);
|
||||
.Returns((Core.Auth.Entities.EmergencyAccess)null);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.ApproveAsync(new Guid(), null));
|
||||
@@ -829,7 +828,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task ApproveAsync_EmergencyAccessGrantorIdNotEquatToApproving_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User grantorUser)
|
||||
{
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated;
|
||||
@@ -851,7 +850,7 @@ public class EmergencyAccessServiceTests
|
||||
public async Task ApproveAsync_EmergencyAccessStatusNotRecoveryInitiated_ThrowsBadRequest(
|
||||
EmergencyAccessStatusType statusType,
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User grantorUser)
|
||||
{
|
||||
emergencyAccess.GrantorId = grantorUser.Id;
|
||||
@@ -869,7 +868,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task ApproveAsync_Success(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User grantorUser,
|
||||
User granteeUser)
|
||||
{
|
||||
@@ -885,20 +884,20 @@ public class EmergencyAccessServiceTests
|
||||
await sutProvider.Sut.ApproveAsync(emergencyAccess.Id, grantorUser);
|
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.Received(1)
|
||||
.ReplaceAsync(Arg.Is<EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.RecoveryApproved));
|
||||
.ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.RecoveryApproved));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RejectAsync_EmergencyAccessIdNull_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User GrantorUser)
|
||||
{
|
||||
emergencyAccess.GrantorId = GrantorUser.Id;
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.Accepted;
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns((EmergencyAccess)null);
|
||||
.Returns((Core.Auth.Entities.EmergencyAccess)null);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser));
|
||||
@@ -909,7 +908,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task RejectAsync_EmergencyAccessGrantorIdNotEqualToRequestUser_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User GrantorUser)
|
||||
{
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.Accepted;
|
||||
@@ -930,7 +929,7 @@ public class EmergencyAccessServiceTests
|
||||
public async Task RejectAsync_EmergencyAccessStatusNotValid_ThrowsBadRequest(
|
||||
EmergencyAccessStatusType statusType,
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User GrantorUser)
|
||||
{
|
||||
emergencyAccess.GrantorId = GrantorUser.Id;
|
||||
@@ -951,7 +950,7 @@ public class EmergencyAccessServiceTests
|
||||
public async Task RejectAsync_Success(
|
||||
EmergencyAccessStatusType statusType,
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User GrantorUser,
|
||||
User GranteeUser)
|
||||
{
|
||||
@@ -968,7 +967,7 @@ public class EmergencyAccessServiceTests
|
||||
|
||||
await sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.Received(1)
|
||||
.ReplaceAsync(Arg.Is<EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Confirmed));
|
||||
.ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Confirmed));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -977,7 +976,7 @@ public class EmergencyAccessServiceTests
|
||||
{
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns((EmergencyAccess)null);
|
||||
.Returns((Core.Auth.Entities.EmergencyAccess)null);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.GetPoliciesAsync(default, default));
|
||||
@@ -992,7 +991,7 @@ public class EmergencyAccessServiceTests
|
||||
public async Task GetPoliciesAsync_RequestNotValidStatusType_ThrowsBadRequest(
|
||||
EmergencyAccessStatusType statusType,
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User granteeUser)
|
||||
{
|
||||
emergencyAccess.GranteeId = granteeUser.Id;
|
||||
@@ -1010,7 +1009,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPoliciesAsync_RequestNotValidType_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User granteeUser)
|
||||
{
|
||||
emergencyAccess.GranteeId = granteeUser.Id;
|
||||
@@ -1032,7 +1031,7 @@ public class EmergencyAccessServiceTests
|
||||
public async Task GetPoliciesAsync_OrganizationUserTypeNotOwner_ReturnsNull(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User granteeUser,
|
||||
User grantorUser,
|
||||
OrganizationUser grantorOrganizationUser)
|
||||
@@ -1062,7 +1061,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPoliciesAsync_OrganizationUserEmpty_ReturnsNull(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User granteeUser,
|
||||
User grantorUser)
|
||||
{
|
||||
@@ -1090,7 +1089,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPoliciesAsync_ReturnsNotNull(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User granteeUser,
|
||||
User grantorUser,
|
||||
OrganizationUser grantorOrganizationUser)
|
||||
@@ -1127,7 +1126,7 @@ public class EmergencyAccessServiceTests
|
||||
{
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns((EmergencyAccess)null);
|
||||
.Returns((Core.Auth.Entities.EmergencyAccess)null);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.TakeoverAsync(default, default));
|
||||
@@ -1138,7 +1137,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task TakeoverAsync_RequestNotValid_GranteeNotEqualToRequestingUser_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User granteeUser)
|
||||
{
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved;
|
||||
@@ -1161,7 +1160,7 @@ public class EmergencyAccessServiceTests
|
||||
public async Task TakeoverAsync_RequestNotValid_StatusType_ThrowsBadRequest(
|
||||
EmergencyAccessStatusType statusType,
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User granteeUser)
|
||||
{
|
||||
emergencyAccess.GranteeId = granteeUser.Id;
|
||||
@@ -1180,7 +1179,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task TakeoverAsync_RequestNotValid_TypeIsView_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User granteeUser)
|
||||
{
|
||||
emergencyAccess.GranteeId = granteeUser.Id;
|
||||
@@ -1203,7 +1202,7 @@ public class EmergencyAccessServiceTests
|
||||
User grantor)
|
||||
{
|
||||
grantor.UsesKeyConnector = true;
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
|
||||
{
|
||||
GrantorId = grantor.Id,
|
||||
GranteeId = granteeUser.Id,
|
||||
@@ -1232,7 +1231,7 @@ public class EmergencyAccessServiceTests
|
||||
User grantor)
|
||||
{
|
||||
grantor.UsesKeyConnector = false;
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
|
||||
{
|
||||
GrantorId = grantor.Id,
|
||||
GranteeId = granteeUser.Id,
|
||||
@@ -1260,7 +1259,7 @@ public class EmergencyAccessServiceTests
|
||||
{
|
||||
sutProvider.GetDependency<IEmergencyAccessRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns((EmergencyAccess)null);
|
||||
.Returns((Core.Auth.Entities.EmergencyAccess)null);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.PasswordAsync(default, default, default, default));
|
||||
@@ -1271,7 +1270,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task PasswordAsync_RequestNotValid_GranteeNotEqualToRequestingUser_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User granteeUser)
|
||||
{
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved;
|
||||
@@ -1294,7 +1293,7 @@ public class EmergencyAccessServiceTests
|
||||
public async Task PasswordAsync_RequestNotValid_StatusType_ThrowsBadRequest(
|
||||
EmergencyAccessStatusType statusType,
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User granteeUser)
|
||||
{
|
||||
emergencyAccess.GranteeId = granteeUser.Id;
|
||||
@@ -1313,7 +1312,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task PasswordAsync_RequestNotValid_TypeIsView_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User granteeUser)
|
||||
{
|
||||
emergencyAccess.GranteeId = granteeUser.Id;
|
||||
@@ -1332,7 +1331,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task PasswordAsync_NonOrgUser_Success(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User granteeUser,
|
||||
User grantorUser,
|
||||
string key,
|
||||
@@ -1367,7 +1366,7 @@ public class EmergencyAccessServiceTests
|
||||
public async Task PasswordAsync_OrgUser_NotOrganizationOwner_RemovedFromOrganization_Success(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User granteeUser,
|
||||
User grantorUser,
|
||||
OrganizationUser organizationUser,
|
||||
@@ -1408,7 +1407,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task PasswordAsync_OrgUser_IsOrganizationOwner_NotRemovedFromOrganization_Success(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User granteeUser,
|
||||
User grantorUser,
|
||||
OrganizationUser organizationUser,
|
||||
@@ -1459,7 +1458,7 @@ public class EmergencyAccessServiceTests
|
||||
Enabled = true
|
||||
}
|
||||
});
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
|
||||
{
|
||||
GrantorId = grantor.Id,
|
||||
GranteeId = requestingUser.Id,
|
||||
@@ -1484,7 +1483,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task ViewAsync_EmergencyAccessTypeNotView_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User granteeUser)
|
||||
{
|
||||
emergencyAccess.GranteeId = granteeUser.Id;
|
||||
@@ -1500,7 +1499,7 @@ public class EmergencyAccessServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetAttachmentDownloadAsync_EmergencyAccessTypeNotView_ThrowsBadRequest(
|
||||
SutProvider<EmergencyAccessService> sutProvider,
|
||||
EmergencyAccess emergencyAccess,
|
||||
Core.Auth.Entities.EmergencyAccess emergencyAccess,
|
||||
User granteeUser)
|
||||
{
|
||||
emergencyAccess.GranteeId = granteeUser.Id;
|
||||
@@ -2,7 +2,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
@@ -23,6 +22,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using EmergencyAccessEntity = Bit.Core.Auth.Entities.EmergencyAccess;
|
||||
|
||||
namespace Bit.Core.Test.Auth.UserFeatures.Registration;
|
||||
|
||||
@@ -726,7 +726,7 @@ public class RegisterUserCommandTests
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_Succeeds(
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,
|
||||
EmergencyAccess emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
|
||||
EmergencyAccessEntity emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = $"test+{Guid.NewGuid()}@example.com";
|
||||
@@ -767,7 +767,7 @@ public class RegisterUserCommandTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_InvalidToken_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user,
|
||||
string masterPasswordHash, EmergencyAccess emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
|
||||
string masterPasswordHash, EmergencyAccessEntity emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = $"test+{Guid.NewGuid()}@example.com";
|
||||
@@ -1112,7 +1112,7 @@ public class RegisterUserCommandTests
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_BlockedDomain_ThrowsBadRequestException(
|
||||
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,
|
||||
EmergencyAccess emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
|
||||
EmergencyAccessEntity emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
|
||||
{
|
||||
// Arrange
|
||||
user.Email = "user@blocked-domain.com";
|
||||
|
||||
@@ -135,6 +135,43 @@ public class ImportCiphersAsyncCommandTests
|
||||
Assert.Equal("You cannot import items into your personal vault because you are a member of an organization which forbids it.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ImportIntoIndividualVaultAsync_FavoriteCiphers_PersistsFavoriteInfo(
|
||||
Guid importingUserId,
|
||||
List<CipherDetails> ciphers,
|
||||
SutProvider<ImportCiphersCommand> sutProvider
|
||||
)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(importingUserId)
|
||||
.Returns(new OrganizationDataOwnershipPolicyRequirement(
|
||||
OrganizationDataOwnershipState.Disabled,
|
||||
[]));
|
||||
|
||||
sutProvider.GetDependency<IFolderRepository>()
|
||||
.GetManyByUserIdAsync(importingUserId)
|
||||
.Returns(new List<Folder>());
|
||||
|
||||
var folders = new List<Folder>();
|
||||
var folderRelationships = new List<KeyValuePair<int, int>>();
|
||||
|
||||
ciphers.ForEach(c =>
|
||||
{
|
||||
c.UserId = importingUserId;
|
||||
c.Favorite = true;
|
||||
});
|
||||
|
||||
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync(importingUserId, Arg.Is<IEnumerable<Cipher>>(ciphers => ciphers.All(c => c.Favorites == $"{{\"{importingUserId.ToString().ToUpperInvariant()}\":true}}")), Arg.Any<List<Folder>>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ImportIntoOrganizationalVaultAsync_Success(
|
||||
Organization organization,
|
||||
|
||||
120
test/Core.Test/Tools/Services/SendValidationServiceTests.cs
Normal file
120
test/Core.Test/Tools/Services/SendValidationServiceTests.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Pricing.Premium;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Tools.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SendValidationServiceTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task StorageRemainingForSendAsync_OrgGrantedPremiumUser_UsesPricingService(
|
||||
SutProvider<SendValidationService> sutProvider,
|
||||
Send send,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
send.UserId = user.Id;
|
||||
send.OrganizationId = null;
|
||||
send.Type = SendType.File;
|
||||
user.Premium = false;
|
||||
user.Storage = 1024L * 1024L * 1024L; // 1 GB used
|
||||
user.EmailVerified = true;
|
||||
|
||||
sutProvider.GetDependency<Bit.Core.Settings.GlobalSettings>().SelfHosted = false;
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).Returns(user);
|
||||
sutProvider.GetDependency<IUserService>().CanAccessPremium(user).Returns(true);
|
||||
|
||||
var premiumPlan = new Plan
|
||||
{
|
||||
Storage = new Purchasable { Provided = 5 }
|
||||
};
|
||||
sutProvider.GetDependency<IPricingClient>().GetAvailablePremiumPlan().Returns(premiumPlan);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.StorageRemainingForSendAsync(send);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IPricingClient>().Received(1).GetAvailablePremiumPlan();
|
||||
Assert.True(result > 0);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task StorageRemainingForSendAsync_IndividualPremium_DoesNotCallPricingService(
|
||||
SutProvider<SendValidationService> sutProvider,
|
||||
Send send,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
send.UserId = user.Id;
|
||||
send.OrganizationId = null;
|
||||
send.Type = SendType.File;
|
||||
user.Premium = true;
|
||||
user.MaxStorageGb = 10;
|
||||
user.EmailVerified = true;
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).Returns(user);
|
||||
sutProvider.GetDependency<IUserService>().CanAccessPremium(user).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.StorageRemainingForSendAsync(send);
|
||||
|
||||
// Assert - should NOT call pricing service for individual premium users
|
||||
await sutProvider.GetDependency<IPricingClient>().DidNotReceive().GetAvailablePremiumPlan();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task StorageRemainingForSendAsync_SelfHosted_DoesNotCallPricingService(
|
||||
SutProvider<SendValidationService> sutProvider,
|
||||
Send send,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
send.UserId = user.Id;
|
||||
send.OrganizationId = null;
|
||||
send.Type = SendType.File;
|
||||
user.Premium = false;
|
||||
user.EmailVerified = true;
|
||||
|
||||
sutProvider.GetDependency<Bit.Core.Settings.GlobalSettings>().SelfHosted = true;
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).Returns(user);
|
||||
sutProvider.GetDependency<IUserService>().CanAccessPremium(user).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.StorageRemainingForSendAsync(send);
|
||||
|
||||
// Assert - should NOT call pricing service for self-hosted
|
||||
await sutProvider.GetDependency<IPricingClient>().DidNotReceive().GetAvailablePremiumPlan();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task StorageRemainingForSendAsync_OrgSend_DoesNotCallPricingService(
|
||||
SutProvider<SendValidationService> sutProvider,
|
||||
Send send,
|
||||
Organization org)
|
||||
{
|
||||
// Arrange
|
||||
send.UserId = null;
|
||||
send.OrganizationId = org.Id;
|
||||
send.Type = SendType.File;
|
||||
org.MaxStorageGb = 100;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.StorageRemainingForSendAsync(send);
|
||||
|
||||
// Assert - should NOT call pricing service for org sends
|
||||
await sutProvider.GetDependency<IPricingClient>().DidNotReceive().GetAvailablePremiumPlan();
|
||||
}
|
||||
}
|
||||
84
test/Core.Test/Utilities/DomainNameAttributeTests.cs
Normal file
84
test/Core.Test/Utilities/DomainNameAttributeTests.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Utilities;
|
||||
|
||||
public class DomainNameValidatorAttributeTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("example.com")] // basic domain
|
||||
[InlineData("sub.example.com")] // subdomain
|
||||
[InlineData("sub.sub2.example.com")] // multiple subdomains
|
||||
[InlineData("example-dash.com")] // domain with dash
|
||||
[InlineData("123example.com")] // domain starting with number
|
||||
[InlineData("example123.com")] // domain with numbers
|
||||
[InlineData("e.com")] // short domain
|
||||
[InlineData("very-long-subdomain-name.example.com")] // long subdomain
|
||||
[InlineData("wörldé.com")] // unicode domain (IDN)
|
||||
public void IsValid_ReturnsTrueWhenValid(string domainName)
|
||||
{
|
||||
var sut = new DomainNameValidatorAttribute();
|
||||
|
||||
var actual = sut.IsValid(domainName);
|
||||
|
||||
Assert.True(actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("<script>alert('xss')</script>")] // XSS attempt
|
||||
[InlineData("example.com<script>")] // XSS suffix
|
||||
[InlineData("<img src=x>")] // HTML tag
|
||||
[InlineData("example.com\t")] // trailing tab
|
||||
[InlineData("\texample.com")] // leading tab
|
||||
[InlineData("exam\tple.com")] // middle tab
|
||||
[InlineData("example.com\n")] // newline
|
||||
[InlineData("example.com\r")] // carriage return
|
||||
[InlineData("example.com\b")] // backspace
|
||||
[InlineData("exam ple.com")] // space in domain
|
||||
[InlineData("example.com ")] // trailing space (after trim, becomes valid, but with space it's invalid)
|
||||
[InlineData(" example.com")] // leading space (after trim, becomes valid, but with space it's invalid)
|
||||
[InlineData("example&.com")] // ampersand
|
||||
[InlineData("example'.com")] // single quote
|
||||
[InlineData("example\".com")] // double quote
|
||||
[InlineData(".example.com")] // starts with dot
|
||||
[InlineData("example.com.")] // ends with dot
|
||||
[InlineData("example..com")] // double dot
|
||||
[InlineData("-example.com")] // starts with dash
|
||||
[InlineData("example-.com")] // label ends with dash
|
||||
[InlineData("")] // empty string
|
||||
[InlineData(" ")] // whitespace only
|
||||
[InlineData("http://example.com")] // URL scheme
|
||||
[InlineData("example.com/path")] // path component
|
||||
[InlineData("user@example.com")] // email format
|
||||
public void IsValid_ReturnsFalseWhenInvalid(string domainName)
|
||||
{
|
||||
var sut = new DomainNameValidatorAttribute();
|
||||
|
||||
var actual = sut.IsValid(domainName);
|
||||
|
||||
Assert.False(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValid_ReturnsTrueWhenNull()
|
||||
{
|
||||
var sut = new DomainNameValidatorAttribute();
|
||||
|
||||
var actual = sut.IsValid(null);
|
||||
|
||||
// Null validation should be handled by [Required] attribute
|
||||
Assert.True(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValid_ReturnsFalseWhenTooLong()
|
||||
{
|
||||
var sut = new DomainNameValidatorAttribute();
|
||||
// Create a domain name longer than 253 characters
|
||||
var longDomain = new string('a', 250) + ".com";
|
||||
|
||||
var actual = sut.IsValid(longDomain);
|
||||
|
||||
Assert.False(actual);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Pricing.Premium;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@@ -2228,10 +2230,6 @@ public class CipherServiceTests
|
||||
.PushSyncCiphersAsync(deletingUserId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[Theory]
|
||||
[OrganizationCipherCustomize]
|
||||
[BitAutoData]
|
||||
@@ -2387,6 +2385,186 @@ public class CipherServiceTests
|
||||
ids.Count() == cipherIds.Length && ids.All(id => cipherIds.Contains(id))));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAttachmentAsync_UserWithOrgGrantedPremium_UsesStorageFromPricingClient(
|
||||
SutProvider<CipherService> sutProvider, CipherDetails cipher, Guid savingUserId)
|
||||
{
|
||||
var stream = new MemoryStream(new byte[100]);
|
||||
var fileName = "test.txt";
|
||||
var key = "test-key";
|
||||
|
||||
// Setup cipher with user ownership
|
||||
cipher.UserId = savingUserId;
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
// Setup user WITHOUT personal premium (Premium = false), but with org-granted premium access
|
||||
var user = new User
|
||||
{
|
||||
Id = savingUserId,
|
||||
Premium = false, // User does not have personal premium
|
||||
MaxStorageGb = null, // No personal storage allocation
|
||||
Storage = 0 // No storage used yet
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(savingUserId)
|
||||
.Returns(user);
|
||||
|
||||
// User has premium access through their organization
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CanAccessPremium(user)
|
||||
.Returns(true);
|
||||
|
||||
// Mock GlobalSettings to indicate cloud (not self-hosted)
|
||||
sutProvider.GetDependency<Bit.Core.Settings.GlobalSettings>().SelfHosted = false;
|
||||
|
||||
// Mock the PricingClient to return a premium plan with 1 GB of storage
|
||||
var premiumPlan = new Plan
|
||||
{
|
||||
Name = "Premium",
|
||||
Available = true,
|
||||
Seat = new Purchasable { StripePriceId = "price_123", Price = 10, Provided = 1 },
|
||||
Storage = new Purchasable { StripePriceId = "price_456", Price = 4, Provided = 1 }
|
||||
};
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetAvailablePremiumPlan()
|
||||
.Returns(premiumPlan);
|
||||
|
||||
sutProvider.GetDependency<IAttachmentStorageService>()
|
||||
.UploadNewAttachmentAsync(Arg.Any<Stream>(), cipher, Arg.Any<CipherAttachment.MetaData>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
sutProvider.GetDependency<IAttachmentStorageService>()
|
||||
.ValidateFileAsync(cipher, Arg.Any<CipherAttachment.MetaData>(), Arg.Any<long>())
|
||||
.Returns((true, 100L));
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.UpdateAttachmentAsync(Arg.Any<CipherAttachment>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.ReplaceAsync(Arg.Any<CipherDetails>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.CreateAttachmentAsync(cipher, stream, fileName, key, 100, savingUserId, false, cipher.RevisionDate);
|
||||
|
||||
// Assert - PricingClient was called to get the premium plan storage
|
||||
await sutProvider.GetDependency<IPricingClient>().Received(1).GetAvailablePremiumPlan();
|
||||
|
||||
// Assert - Attachment was uploaded successfully
|
||||
await sutProvider.GetDependency<IAttachmentStorageService>().Received(1)
|
||||
.UploadNewAttachmentAsync(Arg.Any<Stream>(), cipher, Arg.Any<CipherAttachment.MetaData>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAttachmentAsync_UserWithOrgGrantedPremium_ExceedsStorage_ThrowsBadRequest(
|
||||
SutProvider<CipherService> sutProvider, CipherDetails cipher, Guid savingUserId)
|
||||
{
|
||||
var stream = new MemoryStream(new byte[100]);
|
||||
var fileName = "test.txt";
|
||||
var key = "test-key";
|
||||
|
||||
// Setup cipher with user ownership
|
||||
cipher.UserId = savingUserId;
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
// Setup user WITHOUT personal premium, with org-granted access, but storage is full
|
||||
var user = new User
|
||||
{
|
||||
Id = savingUserId,
|
||||
Premium = false,
|
||||
MaxStorageGb = null,
|
||||
Storage = 1073741824 // 1 GB already used (equals the provided storage)
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(savingUserId)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CanAccessPremium(user)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<Bit.Core.Settings.GlobalSettings>().SelfHosted = false;
|
||||
|
||||
// Premium plan provides 1 GB of storage
|
||||
var premiumPlan = new Plan
|
||||
{
|
||||
Name = "Premium",
|
||||
Available = true,
|
||||
Seat = new Purchasable { StripePriceId = "price_123", Price = 10, Provided = 1 },
|
||||
Storage = new Purchasable { StripePriceId = "price_456", Price = 4, Provided = 1 }
|
||||
};
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetAvailablePremiumPlan()
|
||||
.Returns(premiumPlan);
|
||||
|
||||
// Act & Assert - Should throw because storage is full
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.CreateAttachmentAsync(cipher, stream, fileName, key, 100, savingUserId, false, cipher.RevisionDate));
|
||||
Assert.Contains("Not enough storage available", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAttachmentAsync_UserWithOrgGrantedPremium_SelfHosted_UsesConstantStorage(
|
||||
SutProvider<CipherService> sutProvider, CipherDetails cipher, Guid savingUserId)
|
||||
{
|
||||
var stream = new MemoryStream(new byte[100]);
|
||||
var fileName = "test.txt";
|
||||
var key = "test-key";
|
||||
|
||||
// Setup cipher with user ownership
|
||||
cipher.UserId = savingUserId;
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
// Setup user WITHOUT personal premium, but with org-granted premium access
|
||||
var user = new User
|
||||
{
|
||||
Id = savingUserId,
|
||||
Premium = false,
|
||||
MaxStorageGb = null,
|
||||
Storage = 0
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(savingUserId)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CanAccessPremium(user)
|
||||
.Returns(true);
|
||||
|
||||
// Mock GlobalSettings to indicate self-hosted
|
||||
sutProvider.GetDependency<Bit.Core.Settings.GlobalSettings>().SelfHosted = true;
|
||||
|
||||
sutProvider.GetDependency<IAttachmentStorageService>()
|
||||
.UploadNewAttachmentAsync(Arg.Any<Stream>(), cipher, Arg.Any<CipherAttachment.MetaData>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
sutProvider.GetDependency<IAttachmentStorageService>()
|
||||
.ValidateFileAsync(cipher, Arg.Any<CipherAttachment.MetaData>(), Arg.Any<long>())
|
||||
.Returns((true, 100L));
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.UpdateAttachmentAsync(Arg.Any<CipherAttachment>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.ReplaceAsync(Arg.Any<CipherDetails>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.CreateAttachmentAsync(cipher, stream, fileName, key, 100, savingUserId, false, cipher.RevisionDate);
|
||||
|
||||
// Assert - PricingClient should NOT be called for self-hosted
|
||||
await sutProvider.GetDependency<IPricingClient>().DidNotReceive().GetAvailablePremiumPlan();
|
||||
|
||||
// Assert - Attachment was uploaded successfully
|
||||
await sutProvider.GetDependency<IAttachmentStorageService>().Received(1)
|
||||
.UploadNewAttachmentAsync(Arg.Any<Stream>(), cipher, Arg.Any<CipherAttachment.MetaData>());
|
||||
}
|
||||
|
||||
private async Task AssertNoActionsAsync(SutProvider<CipherService> sutProvider)
|
||||
{
|
||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().GetManyOrganizationDetailsByOrganizationIdAsync(default);
|
||||
|
||||
Reference in New Issue
Block a user