mirror of
https://github.com/bitwarden/server
synced 2026-01-08 11:33:26 +00:00
Merge branch 'main' into auth/pm-22975/client-version-validator
This commit is contained in:
4
.github/ISSUE_TEMPLATE/bw-lite.yml
vendored
4
.github/ISSUE_TEMPLATE/bw-lite.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Bitwarden Lite Deployment Bug Report
|
name: Bitwarden lite Deployment Bug Report
|
||||||
description: File a bug report
|
description: File a bug report
|
||||||
labels: [bug, bw-lite-deploy]
|
labels: [bug, bw-lite-deploy]
|
||||||
body:
|
body:
|
||||||
@@ -74,7 +74,7 @@ body:
|
|||||||
id: epic-label
|
id: epic-label
|
||||||
attributes:
|
attributes:
|
||||||
label: Issue-Link
|
label: Issue-Link
|
||||||
description: Link to our pinned issue, tracking all Bitwarden Lite
|
description: Link to our pinned issue, tracking all Bitwarden lite
|
||||||
value: |
|
value: |
|
||||||
https://github.com/bitwarden/server/issues/2480
|
https://github.com/bitwarden/server/issues/2480
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
2
.github/renovate.json5
vendored
2
.github/renovate.json5
vendored
@@ -63,7 +63,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
matchPackageNames: [
|
matchPackageNames: [
|
||||||
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
|
|
||||||
"DuoUniversal",
|
"DuoUniversal",
|
||||||
"Fido2.AspNet",
|
"Fido2.AspNet",
|
||||||
"Duende.IdentityServer",
|
"Duende.IdentityServer",
|
||||||
@@ -137,6 +136,7 @@
|
|||||||
"AspNetCoreRateLimit",
|
"AspNetCoreRateLimit",
|
||||||
"AspNetCoreRateLimit.Redis",
|
"AspNetCoreRateLimit.Redis",
|
||||||
"Azure.Data.Tables",
|
"Azure.Data.Tables",
|
||||||
|
"Azure.Extensions.AspNetCore.DataProtection.Blobs",
|
||||||
"Azure.Messaging.EventGrid",
|
"Azure.Messaging.EventGrid",
|
||||||
"Azure.Messaging.ServiceBus",
|
"Azure.Messaging.ServiceBus",
|
||||||
"Azure.Storage.Blobs",
|
"Azure.Storage.Blobs",
|
||||||
|
|||||||
49
.github/workflows/build.yml
vendored
49
.github/workflows/build.yml
vendored
@@ -185,13 +185,6 @@ jobs:
|
|||||||
- name: Log in to ACR - production subscription
|
- name: Log in to ACR - production subscription
|
||||||
run: az acr login -n bitwardenprod
|
run: az acr login -n bitwardenprod
|
||||||
|
|
||||||
- name: Retrieve GitHub PAT secrets
|
|
||||||
id: retrieve-secret-pat
|
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
|
||||||
with:
|
|
||||||
keyvault: "bitwarden-ci"
|
|
||||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
|
||||||
|
|
||||||
########## Generate image tag and build Docker image ##########
|
########## Generate image tag and build Docker image ##########
|
||||||
- name: Generate Docker image tag
|
- name: Generate Docker image tag
|
||||||
id: tag
|
id: tag
|
||||||
@@ -250,8 +243,6 @@ jobs:
|
|||||||
linux/arm64
|
linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.image-tags.outputs.tags }}
|
tags: ${{ steps.image-tags.outputs.tags }}
|
||||||
secrets: |
|
|
||||||
"GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}"
|
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
@@ -479,20 +470,29 @@ jobs:
|
|||||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
|
||||||
- name: Retrieve GitHub PAT secrets
|
- name: Get Azure Key Vault secrets
|
||||||
id: retrieve-secret-pat
|
id: get-kv-secrets
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
with:
|
with:
|
||||||
keyvault: "bitwarden-ci"
|
keyvault: gh-org-bitwarden
|
||||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
|
||||||
|
|
||||||
- name: Log out from Azure
|
- name: Log out from Azure
|
||||||
uses: bitwarden/gh-actions/azure-logout@main
|
uses: bitwarden/gh-actions/azure-logout@main
|
||||||
|
|
||||||
- name: Trigger Bitwarden Lite build
|
- name: Generate GH App token
|
||||||
|
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||||
|
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||||
|
owner: ${{ github.repository_owner }}
|
||||||
|
repositories: self-host
|
||||||
|
|
||||||
|
- name: Trigger Bitwarden lite build
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
github-token: ${{ steps.app-token.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
await github.rest.actions.createWorkflowDispatch({
|
await github.rest.actions.createWorkflowDispatch({
|
||||||
owner: 'bitwarden',
|
owner: 'bitwarden',
|
||||||
@@ -520,20 +520,29 @@ jobs:
|
|||||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||||
|
|
||||||
- name: Retrieve GitHub PAT secrets
|
- name: Get Azure Key Vault secrets
|
||||||
id: retrieve-secret-pat
|
id: get-kv-secrets
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
with:
|
with:
|
||||||
keyvault: "bitwarden-ci"
|
keyvault: gh-org-bitwarden
|
||||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
secrets: "BW-GHAPP-ID,BW-GHAPP-KEY"
|
||||||
|
|
||||||
- name: Log out from Azure
|
- name: Log out from Azure
|
||||||
uses: bitwarden/gh-actions/azure-logout@main
|
uses: bitwarden/gh-actions/azure-logout@main
|
||||||
|
|
||||||
|
- name: Generate GH App token
|
||||||
|
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||||
|
private-key: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-KEY }}
|
||||||
|
owner: ${{ github.repository_owner }}
|
||||||
|
repositories: devops
|
||||||
|
|
||||||
- name: Trigger k8s deploy
|
- name: Trigger k8s deploy
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
github-token: ${{ steps.app-token.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
await github.rest.actions.createWorkflowDispatch({
|
await github.rest.actions.createWorkflowDispatch({
|
||||||
owner: 'bitwarden',
|
owner: 'bitwarden',
|
||||||
|
|||||||
4
.github/workflows/test-database.yml
vendored
4
.github/workflows/test-database.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
|||||||
docker compose --profile mssql --profile postgres --profile mysql up -d
|
docker compose --profile mssql --profile postgres --profile mysql up -d
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Add MariaDB for Bitwarden Lite
|
- name: Add MariaDB for Bitwarden lite
|
||||||
# Use a different port than MySQL
|
# Use a different port than MySQL
|
||||||
run: |
|
run: |
|
||||||
docker run --detach --name mariadb --env MARIADB_ROOT_PASSWORD=mariadb-password -p 4306:3306 mariadb:10
|
docker run --detach --name mariadb --env MARIADB_ROOT_PASSWORD=mariadb-password -p 4306:3306 mariadb:10
|
||||||
@@ -133,7 +133,7 @@ jobs:
|
|||||||
# Default Sqlite
|
# Default Sqlite
|
||||||
BW_TEST_DATABASES__3__TYPE: "Sqlite"
|
BW_TEST_DATABASES__3__TYPE: "Sqlite"
|
||||||
BW_TEST_DATABASES__3__CONNECTIONSTRING: "Data Source=${{ runner.temp }}/test.db"
|
BW_TEST_DATABASES__3__CONNECTIONSTRING: "Data Source=${{ runner.temp }}/test.db"
|
||||||
# Bitwarden Lite MariaDB
|
# Bitwarden lite MariaDB
|
||||||
BW_TEST_DATABASES__4__TYPE: "MySql"
|
BW_TEST_DATABASES__4__TYPE: "MySql"
|
||||||
BW_TEST_DATABASES__4__CONNECTIONSTRING: "server=localhost;port=4306;uid=root;pwd=mariadb-password;database=vault_dev;Allow User Variables=true"
|
BW_TEST_DATABASES__4__CONNECTIONSTRING: "server=localhost;port=4306;uid=root;pwd=mariadb-password;database=vault_dev;Allow User Variables=true"
|
||||||
run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
<Version>2025.11.1</Version>
|
<Version>2025.12.0</Version>
|
||||||
|
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
|||||||
@@ -473,6 +473,7 @@ public class OrganizationsController : Controller
|
|||||||
organization.UseOrganizationDomains = model.UseOrganizationDomains;
|
organization.UseOrganizationDomains = model.UseOrganizationDomains;
|
||||||
organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;
|
organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies;
|
||||||
organization.UseAutomaticUserConfirmation = model.UseAutomaticUserConfirmation;
|
organization.UseAutomaticUserConfirmation = model.UseAutomaticUserConfirmation;
|
||||||
|
organization.UsePhishingBlocker = model.UsePhishingBlocker;
|
||||||
|
|
||||||
//secrets
|
//secrets
|
||||||
organization.SmSeats = model.SmSeats;
|
organization.SmSeats = model.SmSeats;
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
|||||||
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
|
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
|
||||||
UseOrganizationDomains = org.UseOrganizationDomains;
|
UseOrganizationDomains = org.UseOrganizationDomains;
|
||||||
UseAutomaticUserConfirmation = org.UseAutomaticUserConfirmation;
|
UseAutomaticUserConfirmation = org.UseAutomaticUserConfirmation;
|
||||||
|
UsePhishingBlocker = org.UsePhishingBlocker;
|
||||||
|
|
||||||
_plans = plans;
|
_plans = plans;
|
||||||
}
|
}
|
||||||
@@ -160,6 +161,8 @@ public class OrganizationEditModel : OrganizationViewModel
|
|||||||
public new bool UseSecretsManager { get; set; }
|
public new bool UseSecretsManager { get; set; }
|
||||||
[Display(Name = "Risk Insights")]
|
[Display(Name = "Risk Insights")]
|
||||||
public new bool UseRiskInsights { get; set; }
|
public new bool UseRiskInsights { get; set; }
|
||||||
|
[Display(Name = "Phishing Blocker")]
|
||||||
|
public new bool UsePhishingBlocker { get; set; }
|
||||||
[Display(Name = "Admin Sponsored Families")]
|
[Display(Name = "Admin Sponsored Families")]
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
[Display(Name = "Self Host")]
|
[Display(Name = "Self Host")]
|
||||||
@@ -327,6 +330,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
|||||||
existingOrganization.SmServiceAccounts = SmServiceAccounts;
|
existingOrganization.SmServiceAccounts = SmServiceAccounts;
|
||||||
existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts;
|
existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts;
|
||||||
existingOrganization.UseOrganizationDomains = UseOrganizationDomains;
|
existingOrganization.UseOrganizationDomains = UseOrganizationDomains;
|
||||||
|
existingOrganization.UsePhishingBlocker = UsePhishingBlocker;
|
||||||
return existingOrganization;
|
return existingOrganization;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ public class OrganizationViewModel
|
|||||||
public int OccupiedSmSeatsCount { get; set; }
|
public int OccupiedSmSeatsCount { get; set; }
|
||||||
public bool UseSecretsManager => Organization.UseSecretsManager;
|
public bool UseSecretsManager => Organization.UseSecretsManager;
|
||||||
public bool UseRiskInsights => Organization.UseRiskInsights;
|
public bool UseRiskInsights => Organization.UseRiskInsights;
|
||||||
|
public bool UsePhishingBlocker => Organization.UsePhishingBlocker;
|
||||||
public IEnumerable<OrganizationUserUserDetails> OwnersDetails { get; set; }
|
public IEnumerable<OrganizationUserUserDetails> OwnersDetails { get; set; }
|
||||||
public IEnumerable<OrganizationUserUserDetails> AdminsDetails { get; set; }
|
public IEnumerable<OrganizationUserUserDetails> AdminsDetails { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,10 @@
|
|||||||
<input type="checkbox" class="form-check-input" asp-for="UseAdminSponsoredFamilies" disabled='@(canEditPlan ? null : "disabled")'>
|
<input type="checkbox" class="form-check-input" asp-for="UseAdminSponsoredFamilies" disabled='@(canEditPlan ? null : "disabled")'>
|
||||||
<label class="form-check-label" asp-for="UseAdminSponsoredFamilies"></label>
|
<label class="form-check-label" asp-for="UseAdminSponsoredFamilies"></label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" asp-for="UsePhishingBlocker" disabled='@(canEditPlan ? null : "disabled")'>
|
||||||
|
<label class="form-check-label" asp-for="UsePhishingBlocker"></label>
|
||||||
|
</div>
|
||||||
@if(FeatureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
|
@if(FeatureService.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers))
|
||||||
{
|
{
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public class DatabaseMigrationHostedService : IHostedService, IDisposable
|
|||||||
public virtual async Task StartAsync(CancellationToken cancellationToken)
|
public virtual async Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Wait 20 seconds to allow database to come online
|
// Wait 20 seconds to allow database to come online
|
||||||
await Task.Delay(20000);
|
await Task.Delay(20000, cancellationToken);
|
||||||
|
|
||||||
var maxMigrationAttempts = 10;
|
var maxMigrationAttempts = 10;
|
||||||
for (var i = 1; i <= maxMigrationAttempts; i++)
|
for (var i = 1; i <= maxMigrationAttempts; i++)
|
||||||
@@ -41,7 +41,7 @@ public class DatabaseMigrationHostedService : IHostedService, IDisposable
|
|||||||
{
|
{
|
||||||
_logger.LogError(e,
|
_logger.LogError(e,
|
||||||
"Database unavailable for migration. Trying again (attempt #{0})...", i + 1);
|
"Database unavailable for migration. Trying again (attempt #{0})...", i + 1);
|
||||||
await Task.Delay(20000);
|
await Task.Delay(20000, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ public abstract class BaseProfileOrganizationResponseModel : ResponseModel
|
|||||||
UseAdminSponsoredFamilies = organizationDetails.UseAdminSponsoredFamilies;
|
UseAdminSponsoredFamilies = organizationDetails.UseAdminSponsoredFamilies;
|
||||||
UseAutomaticUserConfirmation = organizationDetails.UseAutomaticUserConfirmation;
|
UseAutomaticUserConfirmation = organizationDetails.UseAutomaticUserConfirmation;
|
||||||
UseSecretsManager = organizationDetails.UseSecretsManager;
|
UseSecretsManager = organizationDetails.UseSecretsManager;
|
||||||
|
UsePhishingBlocker = organizationDetails.UsePhishingBlocker;
|
||||||
UsePasswordManager = organizationDetails.UsePasswordManager;
|
UsePasswordManager = organizationDetails.UsePasswordManager;
|
||||||
SelfHost = organizationDetails.SelfHost;
|
SelfHost = organizationDetails.SelfHost;
|
||||||
Seats = organizationDetails.Seats;
|
Seats = organizationDetails.Seats;
|
||||||
@@ -99,6 +100,7 @@ public abstract class BaseProfileOrganizationResponseModel : ResponseModel
|
|||||||
public bool UseOrganizationDomains { get; set; }
|
public bool UseOrganizationDomains { get; set; }
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
public bool UseAutomaticUserConfirmation { get; set; }
|
public bool UseAutomaticUserConfirmation { get; set; }
|
||||||
|
public bool UsePhishingBlocker { get; set; }
|
||||||
public bool SelfHost { get; set; }
|
public bool SelfHost { get; set; }
|
||||||
public int? Seats { get; set; }
|
public int? Seats { get; set; }
|
||||||
public short? MaxCollections { get; set; }
|
public short? MaxCollections { get; set; }
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ public class OrganizationResponseModel : ResponseModel
|
|||||||
UseOrganizationDomains = organization.UseOrganizationDomains;
|
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||||
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation;
|
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation;
|
||||||
|
UsePhishingBlocker = organization.UsePhishingBlocker;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
@@ -120,6 +121,7 @@ public class OrganizationResponseModel : ResponseModel
|
|||||||
public bool UseOrganizationDomains { get; set; }
|
public bool UseOrganizationDomains { get; set; }
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
public bool UseAutomaticUserConfirmation { get; set; }
|
public bool UseAutomaticUserConfirmation { get; set; }
|
||||||
|
public bool UsePhishingBlocker { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
|
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
|
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
|
||||||
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.25.0" />
|
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.31.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ public class HibpController : Controller
|
|||||||
}
|
}
|
||||||
else if (response.StatusCode == HttpStatusCode.NotFound)
|
else if (response.StatusCode == HttpStatusCode.NotFound)
|
||||||
{
|
{
|
||||||
return new NotFoundResult();
|
/* 12/1/2025 - Per the HIBP API, If the domain does not have any email addresses in any breaches,
|
||||||
|
an HTTP 404 response will be returned. API also specifies that "404 Not found is the account could
|
||||||
|
not be found and has therefore not been pwned". Per REST semantics we will return 200 OK with empty array. */
|
||||||
|
return Content("[]", "application/json");
|
||||||
}
|
}
|
||||||
else if (response.StatusCode == HttpStatusCode.TooManyRequests && retry)
|
else if (response.StatusCode == HttpStatusCode.TooManyRequests && retry)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -116,4 +116,10 @@ public interface IStripeFacade
|
|||||||
TestClockGetOptions testClockGetOptions = null,
|
TestClockGetOptions testClockGetOptions = null,
|
||||||
RequestOptions requestOptions = null,
|
RequestOptions requestOptions = null,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<Coupon> GetCoupon(
|
||||||
|
string couponId,
|
||||||
|
CouponGetOptions couponGetOptions = null,
|
||||||
|
RequestOptions requestOptions = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public class StripeFacade : IStripeFacade
|
|||||||
private readonly DiscountService _discountService = new();
|
private readonly DiscountService _discountService = new();
|
||||||
private readonly SetupIntentService _setupIntentService = new();
|
private readonly SetupIntentService _setupIntentService = new();
|
||||||
private readonly TestClockService _testClockService = new();
|
private readonly TestClockService _testClockService = new();
|
||||||
|
private readonly CouponService _couponService = new();
|
||||||
|
|
||||||
public async Task<Charge> GetCharge(
|
public async Task<Charge> GetCharge(
|
||||||
string chargeId,
|
string chargeId,
|
||||||
@@ -143,4 +144,11 @@ public class StripeFacade : IStripeFacade
|
|||||||
RequestOptions requestOptions = null,
|
RequestOptions requestOptions = null,
|
||||||
CancellationToken cancellationToken = default) =>
|
CancellationToken cancellationToken = default) =>
|
||||||
_testClockService.GetAsync(testClockId, testClockGetOptions, requestOptions, cancellationToken);
|
_testClockService.GetAsync(testClockId, testClockGetOptions, requestOptions, cancellationToken);
|
||||||
|
|
||||||
|
public Task<Coupon> GetCoupon(
|
||||||
|
string couponId,
|
||||||
|
CouponGetOptions couponGetOptions = null,
|
||||||
|
RequestOptions requestOptions = null,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
_couponService.GetAsync(couponId, couponGetOptions, requestOptions, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using Bit.Core.Billing.Extensions;
|
|||||||
using Bit.Core.Billing.Payment.Queries;
|
using Bit.Core.Billing.Payment.Queries;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Models.Mail.Billing.Renewal.Families2019Renewal;
|
||||||
using Bit.Core.Models.Mail.Billing.Renewal.Families2020Renewal;
|
using Bit.Core.Models.Mail.Billing.Renewal.Families2020Renewal;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
using Bit.Core.Platform.Mail.Mailer;
|
using Bit.Core.Platform.Mail.Mailer;
|
||||||
@@ -284,7 +285,7 @@ public class UpcomingInvoiceHandler(
|
|||||||
{
|
{
|
||||||
await organizationRepository.ReplaceAsync(organization);
|
await organizationRepository.ReplaceAsync(organization);
|
||||||
await stripeFacade.UpdateSubscription(subscription.Id, options);
|
await stripeFacade.UpdateSubscription(subscription.Id, options);
|
||||||
await SendFamiliesRenewalEmailAsync(organization, familiesPlan);
|
await SendFamiliesRenewalEmailAsync(organization, familiesPlan, plan);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
@@ -546,7 +547,18 @@ public class UpcomingInvoiceHandler(
|
|||||||
|
|
||||||
private async Task SendFamiliesRenewalEmailAsync(
|
private async Task SendFamiliesRenewalEmailAsync(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
Plan familiesPlan)
|
Plan familiesPlan,
|
||||||
|
Plan planBeforeAlignment)
|
||||||
|
{
|
||||||
|
await (planBeforeAlignment switch
|
||||||
|
{
|
||||||
|
{ Type: PlanType.FamiliesAnnually2025 } => SendFamilies2020RenewalEmailAsync(organization, familiesPlan),
|
||||||
|
{ Type: PlanType.FamiliesAnnually2019 } => SendFamilies2019RenewalEmailAsync(organization, familiesPlan),
|
||||||
|
_ => throw new InvalidOperationException("Unsupported families plan in SendFamiliesRenewalEmailAsync().")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SendFamilies2020RenewalEmailAsync(Organization organization, Plan familiesPlan)
|
||||||
{
|
{
|
||||||
var email = new Families2020RenewalMail
|
var email = new Families2020RenewalMail
|
||||||
{
|
{
|
||||||
@@ -560,6 +572,36 @@ public class UpcomingInvoiceHandler(
|
|||||||
await mailer.SendEmail(email);
|
await mailer.SendEmail(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SendFamilies2019RenewalEmailAsync(Organization organization, Plan familiesPlan)
|
||||||
|
{
|
||||||
|
var coupon = await stripeFacade.GetCoupon(CouponIDs.Milestone3SubscriptionDiscount);
|
||||||
|
if (coupon == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Coupon for sending families 2019 email id:{CouponIDs.Milestone3SubscriptionDiscount} not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coupon.PercentOff == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"coupon.PercentOff for sending families 2019 email id:{CouponIDs.Milestone3SubscriptionDiscount} is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
var discountedAnnualRenewalPrice = familiesPlan.PasswordManager.BasePrice * (100 - coupon.PercentOff.Value) / 100;
|
||||||
|
|
||||||
|
var email = new Families2019RenewalMail
|
||||||
|
{
|
||||||
|
ToEmails = [organization.BillingEmail],
|
||||||
|
View = new Families2019RenewalMailView
|
||||||
|
{
|
||||||
|
BaseMonthlyRenewalPrice = (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US")),
|
||||||
|
BaseAnnualRenewalPrice = familiesPlan.PasswordManager.BasePrice.ToString("C", new CultureInfo("en-US")),
|
||||||
|
DiscountAmount = $"{coupon.PercentOff}%",
|
||||||
|
DiscountedAnnualRenewalPrice = discountedAnnualRenewalPrice.ToString("C", new CultureInfo("en-US"))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await mailer.SendEmail(email);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task SendPremiumRenewalEmailAsync(
|
private async Task SendPremiumRenewalEmailAsync(
|
||||||
User user,
|
User user,
|
||||||
PremiumPlan premiumPlan)
|
PremiumPlan premiumPlan)
|
||||||
|
|||||||
@@ -134,6 +134,11 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool UseAutomaticUserConfirmation { get; set; }
|
public bool UseAutomaticUserConfirmation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If set to true, the organization has phishing protection enabled.
|
||||||
|
/// </summary>
|
||||||
|
public bool UsePhishingBlocker { get; set; }
|
||||||
|
|
||||||
public void SetNewId()
|
public void SetNewId()
|
||||||
{
|
{
|
||||||
if (Id == default(Guid))
|
if (Id == default(Guid))
|
||||||
@@ -334,5 +339,6 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable
|
|||||||
UseOrganizationDomains = license.UseOrganizationDomains;
|
UseOrganizationDomains = license.UseOrganizationDomains;
|
||||||
UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies;
|
UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies;
|
||||||
UseAutomaticUserConfirmation = license.UseAutomaticUserConfirmation;
|
UseAutomaticUserConfirmation = license.UseAutomaticUserConfirmation;
|
||||||
|
UsePhishingBlocker = license.UsePhishingBlocker;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,4 +53,5 @@ public interface IProfileOrganizationDetails
|
|||||||
bool UseAdminSponsoredFamilies { get; set; }
|
bool UseAdminSponsoredFamilies { get; set; }
|
||||||
bool UseOrganizationDomains { get; set; }
|
bool UseOrganizationDomains { get; set; }
|
||||||
bool UseAutomaticUserConfirmation { get; set; }
|
bool UseAutomaticUserConfirmation { get; set; }
|
||||||
|
bool UsePhishingBlocker { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public class OrganizationAbility
|
|||||||
UseOrganizationDomains = organization.UseOrganizationDomains;
|
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||||
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation;
|
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation;
|
||||||
|
UsePhishingBlocker = organization.UsePhishingBlocker;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
@@ -51,4 +52,5 @@ public class OrganizationAbility
|
|||||||
public bool UseOrganizationDomains { get; set; }
|
public bool UseOrganizationDomains { get; set; }
|
||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
public bool UseAutomaticUserConfirmation { get; set; }
|
public bool UseAutomaticUserConfirmation { get; set; }
|
||||||
|
public bool UsePhishingBlocker { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,4 +65,5 @@ public class OrganizationUserOrganizationDetails : IProfileOrganizationDetails
|
|||||||
public bool UseAdminSponsoredFamilies { get; set; }
|
public bool UseAdminSponsoredFamilies { get; set; }
|
||||||
public bool? IsAdminInitiated { get; set; }
|
public bool? IsAdminInitiated { get; set; }
|
||||||
public bool UseAutomaticUserConfirmation { get; set; }
|
public bool UseAutomaticUserConfirmation { get; set; }
|
||||||
|
public bool UsePhishingBlocker { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ public class SelfHostedOrganizationDetails : Organization
|
|||||||
Status = Status,
|
Status = Status,
|
||||||
UseRiskInsights = UseRiskInsights,
|
UseRiskInsights = UseRiskInsights,
|
||||||
UseAdminSponsoredFamilies = UseAdminSponsoredFamilies,
|
UseAdminSponsoredFamilies = UseAdminSponsoredFamilies,
|
||||||
|
UsePhishingBlocker = UsePhishingBlocker,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,4 +56,5 @@ public class ProviderUserOrganizationDetails : IProfileOrganizationDetails
|
|||||||
public string? SsoExternalId { get; set; }
|
public string? SsoExternalId { get; set; }
|
||||||
public string? Permissions { get; set; }
|
public string? Permissions { get; set; }
|
||||||
public string? ResetPasswordKey { get; set; }
|
public string? ResetPasswordKey { get; set; }
|
||||||
|
public bool UsePhishingBlocker { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Automatic User Confirmation
|
||||||
|
|
||||||
|
Owned by: admin-console
|
||||||
|
|
||||||
|
Automatic confirmation requests are server driven events that are sent to the admin's client where via a background service the confirmation will occur. The basic model
|
||||||
|
for the workflow is as follows:
|
||||||
|
|
||||||
|
- The Api server sends an invite email to a user.
|
||||||
|
- The user accepts the invite request, which is sent back to the Api server
|
||||||
|
- The Api server sends a push-notification with the OrganizationId and UserId to a client admin session.
|
||||||
|
- The Client performs the key exchange in the background and POSTs the ConfirmRequest back to the Api server
|
||||||
|
- The Api server runs the OrgUser_Confirm sproc to confirm the user in the DB
|
||||||
|
|
||||||
|
This Feature has the following security measures in place in order to achieve our security goals:
|
||||||
|
|
||||||
|
- The single organization exemption for admins/owners is removed for this policy.
|
||||||
|
- This is enforced by preventing enabling the policy and organization plan feature if there are non-compliant users
|
||||||
|
- Emergency access is removed for all organization users
|
||||||
|
- Automatic confirmation will only apply to the User role (You cannot auto confirm admins/owners to an organization)
|
||||||
|
- The organization has no members with the Provider user type.
|
||||||
|
- This will also prevent the policy and organization plan feature from being enabled
|
||||||
|
- This will prevent sending organization invites to provider users
|
||||||
@@ -4,6 +4,8 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models;
|
||||||
|
using Bit.Core.Platform.Push;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||||
@@ -16,19 +18,22 @@ public class SavePolicyCommand : ISavePolicyCommand
|
|||||||
private readonly IReadOnlyDictionary<PolicyType, IPolicyValidator> _policyValidators;
|
private readonly IReadOnlyDictionary<PolicyType, IPolicyValidator> _policyValidators;
|
||||||
private readonly TimeProvider _timeProvider;
|
private readonly TimeProvider _timeProvider;
|
||||||
private readonly IPostSavePolicySideEffect _postSavePolicySideEffect;
|
private readonly IPostSavePolicySideEffect _postSavePolicySideEffect;
|
||||||
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
|
|
||||||
public SavePolicyCommand(IApplicationCacheService applicationCacheService,
|
public SavePolicyCommand(IApplicationCacheService applicationCacheService,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
IEnumerable<IPolicyValidator> policyValidators,
|
IEnumerable<IPolicyValidator> policyValidators,
|
||||||
TimeProvider timeProvider,
|
TimeProvider timeProvider,
|
||||||
IPostSavePolicySideEffect postSavePolicySideEffect)
|
IPostSavePolicySideEffect postSavePolicySideEffect,
|
||||||
|
IPushNotificationService pushNotificationService)
|
||||||
{
|
{
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_policyRepository = policyRepository;
|
_policyRepository = policyRepository;
|
||||||
_timeProvider = timeProvider;
|
_timeProvider = timeProvider;
|
||||||
_postSavePolicySideEffect = postSavePolicySideEffect;
|
_postSavePolicySideEffect = postSavePolicySideEffect;
|
||||||
|
_pushNotificationService = pushNotificationService;
|
||||||
|
|
||||||
var policyValidatorsDict = new Dictionary<PolicyType, IPolicyValidator>();
|
var policyValidatorsDict = new Dictionary<PolicyType, IPolicyValidator>();
|
||||||
foreach (var policyValidator in policyValidators)
|
foreach (var policyValidator in policyValidators)
|
||||||
@@ -75,6 +80,8 @@ public class SavePolicyCommand : ISavePolicyCommand
|
|||||||
await _policyRepository.UpsertAsync(policy);
|
await _policyRepository.UpsertAsync(policy);
|
||||||
await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);
|
await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);
|
||||||
|
|
||||||
|
await PushPolicyUpdateToClients(policy.OrganizationId, policy);
|
||||||
|
|
||||||
return policy;
|
return policy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,4 +159,17 @@ public class SavePolicyCommand : ISavePolicyCommand
|
|||||||
var currentPolicy = savedPoliciesDict.GetValueOrDefault(policyUpdate.Type);
|
var currentPolicy = savedPoliciesDict.GetValueOrDefault(policyUpdate.Type);
|
||||||
return (savedPoliciesDict, currentPolicy);
|
return (savedPoliciesDict, currentPolicy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Task PushPolicyUpdateToClients(Guid organizationId, Policy policy) => this._pushNotificationService.PushAsync(new PushNotification<SyncPolicyPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.PolicyChanged,
|
||||||
|
Target = NotificationTarget.Organization,
|
||||||
|
TargetId = organizationId,
|
||||||
|
ExcludeCurrentContext = false,
|
||||||
|
Payload = new SyncPolicyPushNotification
|
||||||
|
{
|
||||||
|
Policy = policy,
|
||||||
|
OrganizationId = organizationId
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Int
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models;
|
||||||
|
using Bit.Core.Platform.Push;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||||
@@ -15,7 +17,8 @@ public class VNextSavePolicyCommand(
|
|||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
IEnumerable<IPolicyUpdateEvent> policyUpdateEventHandlers,
|
IEnumerable<IPolicyUpdateEvent> policyUpdateEventHandlers,
|
||||||
TimeProvider timeProvider,
|
TimeProvider timeProvider,
|
||||||
IPolicyEventHandlerFactory policyEventHandlerFactory)
|
IPolicyEventHandlerFactory policyEventHandlerFactory,
|
||||||
|
IPushNotificationService pushNotificationService)
|
||||||
: IVNextSavePolicyCommand
|
: IVNextSavePolicyCommand
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -74,7 +77,7 @@ public class VNextSavePolicyCommand(
|
|||||||
policy.RevisionDate = timeProvider.GetUtcNow().UtcDateTime;
|
policy.RevisionDate = timeProvider.GetUtcNow().UtcDateTime;
|
||||||
|
|
||||||
await policyRepository.UpsertAsync(policy);
|
await policyRepository.UpsertAsync(policy);
|
||||||
|
await PushPolicyUpdateToClients(policyUpdateRequest.OrganizationId, policy);
|
||||||
return policy;
|
return policy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,4 +195,17 @@ public class VNextSavePolicyCommand(
|
|||||||
var savedPoliciesDict = savedPolicies.ToDictionary(p => p.Type);
|
var savedPoliciesDict = savedPolicies.ToDictionary(p => p.Type);
|
||||||
return savedPoliciesDict;
|
return savedPoliciesDict;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Task PushPolicyUpdateToClients(Guid organizationId, Policy policy) => pushNotificationService.PushAsync(new PushNotification<SyncPolicyPushNotification>
|
||||||
|
{
|
||||||
|
Type = PushType.PolicyChanged,
|
||||||
|
Target = NotificationTarget.Organization,
|
||||||
|
TargetId = organizationId,
|
||||||
|
ExcludeCurrentContext = false,
|
||||||
|
Payload = new SyncPolicyPushNotification
|
||||||
|
{
|
||||||
|
Policy = policy,
|
||||||
|
OrganizationId = organizationId
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public static class PolicyServiceCollectionExtensions
|
|||||||
services.AddScoped<IPolicyValidator, MaximumVaultTimeoutPolicyValidator>();
|
services.AddScoped<IPolicyValidator, MaximumVaultTimeoutPolicyValidator>();
|
||||||
services.AddScoped<IPolicyValidator, UriMatchDefaultPolicyValidator>();
|
services.AddScoped<IPolicyValidator, UriMatchDefaultPolicyValidator>();
|
||||||
services.AddScoped<IPolicyValidator, FreeFamiliesForEnterprisePolicyValidator>();
|
services.AddScoped<IPolicyValidator, FreeFamiliesForEnterprisePolicyValidator>();
|
||||||
|
services.AddScoped<IPolicyValidator, AutomaticUserConfirmationPolicyEventHandler>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete("Use AddPolicyUpdateEvents instead.")]
|
[Obsolete("Use AddPolicyUpdateEvents instead.")]
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ public static class OrganizationFactory
|
|||||||
UseAdminSponsoredFamilies =
|
UseAdminSponsoredFamilies =
|
||||||
claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseAdminSponsoredFamilies),
|
claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseAdminSponsoredFamilies),
|
||||||
UseAutomaticUserConfirmation = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseAutomaticUserConfirmation),
|
UseAutomaticUserConfirmation = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UseAutomaticUserConfirmation),
|
||||||
|
UsePhishingBlocker = claimsPrincipal.GetValue<bool>(OrganizationLicenseConstants.UsePhishingBlocker),
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Organization Create(
|
public static Organization Create(
|
||||||
@@ -111,6 +112,7 @@ public static class OrganizationFactory
|
|||||||
UseRiskInsights = license.UseRiskInsights,
|
UseRiskInsights = license.UseRiskInsights,
|
||||||
UseOrganizationDomains = license.UseOrganizationDomains,
|
UseOrganizationDomains = license.UseOrganizationDomains,
|
||||||
UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies,
|
UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies,
|
||||||
UseAutomaticUserConfirmation = license.UseAutomaticUserConfirmation
|
UseAutomaticUserConfirmation = license.UseAutomaticUserConfirmation,
|
||||||
|
UsePhishingBlocker = license.UsePhishingBlocker,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/Core/Auth/Sso/IUserSsoOrganizationIdentifierQuery.cs
Normal file
23
src/Core/Auth/Sso/IUserSsoOrganizationIdentifierQuery.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Auth.Sso;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query to retrieve the SSO organization identifier that a user is a confirmed member of.
|
||||||
|
/// </summary>
|
||||||
|
public interface IUserSsoOrganizationIdentifierQuery
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the SSO organization identifier for a confirmed organization user.
|
||||||
|
/// If there is more than one organization a User is associated with, we return null. If there are more than one
|
||||||
|
/// organization there is no way to know which organization the user wishes to authenticate with.
|
||||||
|
/// Owners and Admins who are not subject to the SSO required policy cannot utilize this flow, since they may have
|
||||||
|
/// multiple organizations with different SSO configurations.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The ID of the <see cref="User"/> to retrieve the SSO organization for. _Not_ an <see cref="OrganizationUser"/>.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The organization identifier if the user is a confirmed member of an organization with SSO configured,
|
||||||
|
/// otherwise null
|
||||||
|
/// </returns>
|
||||||
|
Task<string?> GetSsoOrganizationIdentifierAsync(Guid userId);
|
||||||
|
}
|
||||||
38
src/Core/Auth/Sso/UserSsoOrganizationIdentifierQuery.cs
Normal file
38
src/Core/Auth/Sso/UserSsoOrganizationIdentifierQuery.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Core.Auth.Sso;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// TODO : PM-28846 review data structures as they relate to this query
|
||||||
|
/// Query to retrieve the SSO organization identifier that a user is a confirmed member of.
|
||||||
|
/// </summary>
|
||||||
|
public class UserSsoOrganizationIdentifierQuery(
|
||||||
|
IOrganizationUserRepository _organizationUserRepository,
|
||||||
|
IOrganizationRepository _organizationRepository) : IUserSsoOrganizationIdentifierQuery
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<string?> GetSsoOrganizationIdentifierAsync(Guid userId)
|
||||||
|
{
|
||||||
|
// Get all confirmed organization memberships for the user
|
||||||
|
var organizationUsers = await _organizationUserRepository.GetManyByUserAsync(userId);
|
||||||
|
|
||||||
|
// we can only confidently return the correct SsoOrganizationIdentifier if there is exactly one Organization.
|
||||||
|
// The user must also be in the Confirmed status.
|
||||||
|
var confirmedOrgUsers = organizationUsers.Where(ou => ou.Status == OrganizationUserStatusType.Confirmed);
|
||||||
|
if (confirmedOrgUsers.Count() != 1)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var confirmedOrgUser = confirmedOrgUsers.Single();
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(confirmedOrgUser.OrganizationId);
|
||||||
|
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return organization.Identifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
|
using Bit.Core.Auth.Sso;
|
||||||
|
|
||||||
using Bit.Core.Auth.UserFeatures.DeviceTrust;
|
using Bit.Core.Auth.UserFeatures.DeviceTrust;
|
||||||
using Bit.Core.Auth.UserFeatures.Registration;
|
using Bit.Core.Auth.UserFeatures.Registration;
|
||||||
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
||||||
@@ -29,6 +28,7 @@ public static class UserServiceCollectionExtensions
|
|||||||
services.AddWebAuthnLoginCommands();
|
services.AddWebAuthnLoginCommands();
|
||||||
services.AddTdeOffboardingPasswordCommands();
|
services.AddTdeOffboardingPasswordCommands();
|
||||||
services.AddTwoFactorQueries();
|
services.AddTwoFactorQueries();
|
||||||
|
services.AddSsoQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddDeviceTrustCommands(this IServiceCollection services)
|
public static void AddDeviceTrustCommands(this IServiceCollection services)
|
||||||
@@ -69,4 +69,9 @@ public static class UserServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
services.AddScoped<ITwoFactorIsEnabledQuery, TwoFactorIsEnabledQuery>();
|
services.AddScoped<ITwoFactorIsEnabledQuery, TwoFactorIsEnabledQuery>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AddSsoQueries(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<IUserSsoOrganizationIdentifierQuery, UserSsoOrganizationIdentifierQuery>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public static class OrganizationLicenseConstants
|
|||||||
public const string UseAdminSponsoredFamilies = nameof(UseAdminSponsoredFamilies);
|
public const string UseAdminSponsoredFamilies = nameof(UseAdminSponsoredFamilies);
|
||||||
public const string UseOrganizationDomains = nameof(UseOrganizationDomains);
|
public const string UseOrganizationDomains = nameof(UseOrganizationDomains);
|
||||||
public const string UseAutomaticUserConfirmation = nameof(UseAutomaticUserConfirmation);
|
public const string UseAutomaticUserConfirmation = nameof(UseAutomaticUserConfirmation);
|
||||||
|
public const string UsePhishingBlocker = nameof(UsePhishingBlocker);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class UserLicenseConstants
|
public static class UserLicenseConstants
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organizati
|
|||||||
new(nameof(OrganizationLicenseConstants.UseAdminSponsoredFamilies), entity.UseAdminSponsoredFamilies.ToString()),
|
new(nameof(OrganizationLicenseConstants.UseAdminSponsoredFamilies), entity.UseAdminSponsoredFamilies.ToString()),
|
||||||
new(nameof(OrganizationLicenseConstants.UseOrganizationDomains), entity.UseOrganizationDomains.ToString()),
|
new(nameof(OrganizationLicenseConstants.UseOrganizationDomains), entity.UseOrganizationDomains.ToString()),
|
||||||
new(nameof(OrganizationLicenseConstants.UseAutomaticUserConfirmation), entity.UseAutomaticUserConfirmation.ToString()),
|
new(nameof(OrganizationLicenseConstants.UseAutomaticUserConfirmation), entity.UseAutomaticUserConfirmation.ToString()),
|
||||||
|
new(nameof(OrganizationLicenseConstants.UsePhishingBlocker), entity.UsePhishingBlocker.ToString()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (entity.Name is not null)
|
if (entity.Name is not null)
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ public class OrganizationLicense : ILicense
|
|||||||
public int? SmSeats { get; set; }
|
public int? SmSeats { get; set; }
|
||||||
public int? SmServiceAccounts { get; set; }
|
public int? SmServiceAccounts { get; set; }
|
||||||
public bool UseRiskInsights { get; set; }
|
public bool UseRiskInsights { get; set; }
|
||||||
|
public bool UsePhishingBlocker { get; set; }
|
||||||
|
|
||||||
// Deprecated. Left for backwards compatibility with old license versions.
|
// Deprecated. Left for backwards compatibility with old license versions.
|
||||||
public bool LimitCollectionCreationDeletion { get; set; } = true;
|
public bool LimitCollectionCreationDeletion { get; set; } = true;
|
||||||
@@ -228,7 +229,8 @@ public class OrganizationLicense : ILicense
|
|||||||
!p.Name.Equals(nameof(UseRiskInsights)) &&
|
!p.Name.Equals(nameof(UseRiskInsights)) &&
|
||||||
!p.Name.Equals(nameof(UseAdminSponsoredFamilies)) &&
|
!p.Name.Equals(nameof(UseAdminSponsoredFamilies)) &&
|
||||||
!p.Name.Equals(nameof(UseOrganizationDomains)) &&
|
!p.Name.Equals(nameof(UseOrganizationDomains)) &&
|
||||||
!p.Name.Equals(nameof(UseAutomaticUserConfirmation)))
|
!p.Name.Equals(nameof(UseAutomaticUserConfirmation)) &&
|
||||||
|
!p.Name.Equals(nameof(UsePhishingBlocker)))
|
||||||
.OrderBy(p => p.Name)
|
.OrderBy(p => p.Name)
|
||||||
.Select(p => $"{p.Name}:{Core.Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}")
|
.Select(p => $"{p.Name}:{Core.Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}")
|
||||||
.Aggregate((c, n) => $"{c}|{n}");
|
.Aggregate((c, n) => $"{c}|{n}");
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string MJMLBasedEmailTemplates = "mjml-based-email-templates";
|
public const string MJMLBasedEmailTemplates = "mjml-based-email-templates";
|
||||||
public const string MjmlWelcomeEmailTemplates = "pm-21741-mjml-welcome-email";
|
public const string MjmlWelcomeEmailTemplates = "pm-21741-mjml-welcome-email";
|
||||||
public const string MarketingInitiatedPremiumFlow = "pm-26140-marketing-initiated-premium-flow";
|
public const string MarketingInitiatedPremiumFlow = "pm-26140-marketing-initiated-premium-flow";
|
||||||
|
public const string RedirectOnSsoRequired = "pm-1632-redirect-on-sso-required";
|
||||||
|
|
||||||
/* Autofill Team */
|
/* Autofill Team */
|
||||||
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
||||||
@@ -202,14 +203,11 @@ public static class FeatureFlagKeys
|
|||||||
|
|
||||||
/* Key Management Team */
|
/* Key Management Team */
|
||||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||||
public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service";
|
|
||||||
public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration";
|
public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration";
|
||||||
public const string Argon2Default = "argon2-default";
|
public const string Argon2Default = "argon2-default";
|
||||||
public const string UserkeyRotationV2 = "userkey-rotation-v2";
|
public const string UserkeyRotationV2 = "userkey-rotation-v2";
|
||||||
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
|
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
|
||||||
public const string UserSdkForDecryption = "use-sdk-for-decryption";
|
|
||||||
public const string EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation";
|
public const string EnrollAeadOnKeyRotation = "enroll-aead-on-key-rotation";
|
||||||
public const string PM17987_BlockType0 = "pm-17987-block-type-0";
|
|
||||||
public const string ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings";
|
public const string ForceUpdateKDFSettings = "pm-18021-force-update-kdf-settings";
|
||||||
public const string UnlockWithMasterPasswordUnlockData = "pm-23246-unlock-with-master-password-unlock-data";
|
public const string UnlockWithMasterPasswordUnlockData = "pm-23246-unlock-with-master-password-unlock-data";
|
||||||
public const string WindowsBiometricsV2 = "pm-25373-windows-biometrics-v2";
|
public const string WindowsBiometricsV2 = "pm-25373-windows-biometrics-v2";
|
||||||
|
|||||||
@@ -38,10 +38,6 @@ public class CurrentContext(
|
|||||||
public virtual List<CurrentContextProvider> Providers { get; set; }
|
public virtual List<CurrentContextProvider> Providers { get; set; }
|
||||||
public virtual Guid? InstallationId { get; set; }
|
public virtual Guid? InstallationId { get; set; }
|
||||||
public virtual Guid? OrganizationId { get; set; }
|
public virtual Guid? OrganizationId { get; set; }
|
||||||
public virtual bool CloudflareWorkerProxied { get; set; }
|
|
||||||
public virtual bool IsBot { get; set; }
|
|
||||||
public virtual bool MaybeBot { get; set; }
|
|
||||||
public virtual int? BotScore { get; set; }
|
|
||||||
public virtual string ClientId { get; set; }
|
public virtual string ClientId { get; set; }
|
||||||
public virtual Version ClientVersion { get; set; }
|
public virtual Version ClientVersion { get; set; }
|
||||||
public virtual bool ClientVersionIsPrerelease { get; set; }
|
public virtual bool ClientVersionIsPrerelease { get; set; }
|
||||||
@@ -70,27 +66,6 @@ public class CurrentContext(
|
|||||||
DeviceType = dType;
|
DeviceType = dType;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!BotScore.HasValue && httpContext.Request.Headers.TryGetValue("X-Cf-Bot-Score", out var cfBotScore) &&
|
|
||||||
int.TryParse(cfBotScore, out var parsedBotScore))
|
|
||||||
{
|
|
||||||
BotScore = parsedBotScore;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (httpContext.Request.Headers.TryGetValue("X-Cf-Worked-Proxied", out var cfWorkedProxied))
|
|
||||||
{
|
|
||||||
CloudflareWorkerProxied = cfWorkedProxied == "1";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (httpContext.Request.Headers.TryGetValue("X-Cf-Is-Bot", out var cfIsBot))
|
|
||||||
{
|
|
||||||
IsBot = cfIsBot == "1";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (httpContext.Request.Headers.TryGetValue("X-Cf-Maybe-Bot", out var cfMaybeBot))
|
|
||||||
{
|
|
||||||
MaybeBot = cfMaybeBot == "1";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (httpContext.Request.Headers.TryGetValue("Bitwarden-Client-Version", out var bitWardenClientVersion) && Version.TryParse(bitWardenClientVersion, out var cVersion))
|
if (httpContext.Request.Headers.TryGetValue("Bitwarden-Client-Version", out var bitWardenClientVersion) && Version.TryParse(bitWardenClientVersion, out var cVersion))
|
||||||
{
|
{
|
||||||
ClientVersion = cVersion;
|
ClientVersion = cVersion;
|
||||||
|
|||||||
@@ -31,9 +31,6 @@ public interface ICurrentContext
|
|||||||
Guid? InstallationId { get; set; }
|
Guid? InstallationId { get; set; }
|
||||||
Guid? OrganizationId { get; set; }
|
Guid? OrganizationId { get; set; }
|
||||||
IdentityClientType IdentityClientType { get; set; }
|
IdentityClientType IdentityClientType { get; set; }
|
||||||
bool IsBot { get; set; }
|
|
||||||
bool MaybeBot { get; set; }
|
|
||||||
int? BotScore { get; set; }
|
|
||||||
string ClientId { get; set; }
|
string ClientId { get; set; }
|
||||||
Version ClientVersion { get; set; }
|
Version ClientVersion { get; set; }
|
||||||
bool ClientVersionIsPrerelease { get; set; }
|
bool ClientVersionIsPrerelease { get; set; }
|
||||||
|
|||||||
@@ -25,12 +25,12 @@
|
|||||||
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
||||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="4.0.1.3" />
|
<PackageReference Include="AWSSDK.SimpleEmail" Version="4.0.1.3" />
|
||||||
<PackageReference Include="AWSSDK.SQS" Version="4.0.1.5" />
|
<PackageReference Include="AWSSDK.SQS" Version="4.0.1.5" />
|
||||||
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
<PackageReference Include="Azure.Data.Tables" Version="12.11.0" />
|
||||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
||||||
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.18.1" />
|
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.20.1" />
|
||||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.21.2" />
|
<PackageReference Include="Azure.Storage.Blobs" Version="12.26.0" />
|
||||||
<PackageReference Include="Azure.Storage.Queues" Version="12.19.1" />
|
<PackageReference Include="Azure.Storage.Queues" Version="12.24.0" />
|
||||||
<PackageReference Include="BitPay.Light" Version="1.0.1907" />
|
<PackageReference Include="BitPay.Light" Version="1.0.1907" />
|
||||||
<PackageReference Include="DuoUniversal" Version="1.3.1" />
|
<PackageReference Include="DuoUniversal" Version="1.3.1" />
|
||||||
<PackageReference Include="DnsClient" Version="1.8.0" />
|
<PackageReference Include="DnsClient" Version="1.8.0" />
|
||||||
@@ -60,9 +60,9 @@
|
|||||||
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.10" />
|
||||||
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.10.4" />
|
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.10.4" />
|
||||||
<PackageReference Include="Quartz" Version="3.14.0" />
|
<PackageReference Include="Quartz" Version="3.15.1" />
|
||||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" />
|
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.1" />
|
||||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.14.0" />
|
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.15.1" />
|
||||||
<PackageReference Include="RabbitMQ.Client" Version="7.1.2" />
|
<PackageReference Include="RabbitMQ.Client" Version="7.1.2" />
|
||||||
<PackageReference Include="ZiggyCreatures.FusionCache" Version="2.0.2" />
|
<PackageReference Include="ZiggyCreatures.FusionCache" Version="2.0.2" />
|
||||||
<PackageReference Include="ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis" Version="2.0.2" />
|
<PackageReference Include="ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis" Version="2.0.2" />
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<mjml>
|
||||||
|
<mj-head>
|
||||||
|
<mj-include path="../../../components/head.mjml"/>
|
||||||
|
</mj-head>
|
||||||
|
|
||||||
|
<!-- Blue Header Section-->
|
||||||
|
<mj-body css-class="border-fix">
|
||||||
|
<mj-wrapper css-class="border-fix" padding="20px 20px 0px 20px">
|
||||||
|
<mj-bw-simple-hero />
|
||||||
|
</mj-wrapper>
|
||||||
|
|
||||||
|
<!-- Main Content Section -->
|
||||||
|
<mj-wrapper padding="0px 20px 0px 20px">
|
||||||
|
<mj-section background-color="#fff" padding="15px 10px 10px 10px">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text font-size="16px" line-height="24px" padding="10px 15px 15px 15px">
|
||||||
|
Your Bitwarden Families subscription renews in 15 days. The price is updating to {{BaseMonthlyRenewalPrice}}/month, billed annually
|
||||||
|
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 renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.
|
||||||
|
</mj-text>
|
||||||
|
<mj-text font-size="16px" line-height="24px" padding="10px 15px">
|
||||||
|
Questions? Contact
|
||||||
|
<a href="mailto:support@bitwarden.com" class="link">support@bitwarden.com</a>
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
<mj-section background-color="#fff" padding="0 20px 20px 20px">
|
||||||
|
</mj-section>
|
||||||
|
</mj-wrapper>
|
||||||
|
|
||||||
|
<!-- Learn More Section -->
|
||||||
|
<mj-wrapper padding="0px 20px 10px 20px">
|
||||||
|
<mj-bw-learn-more-footer/>
|
||||||
|
</mj-wrapper>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<mj-include path="../../../components/footer.mjml"/>
|
||||||
|
</mj-body>
|
||||||
|
</mjml>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using Bit.Core.Platform.Mail.Mailer;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.Mail.Billing.Renewal.Families2019Renewal;
|
||||||
|
|
||||||
|
public class Families2019RenewalMailView : BaseMailView
|
||||||
|
{
|
||||||
|
public required string BaseMonthlyRenewalPrice { get; set; }
|
||||||
|
public required string BaseAnnualRenewalPrice { get; set; }
|
||||||
|
public required string DiscountedAnnualRenewalPrice { get; set; }
|
||||||
|
public required string DiscountAmount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Families2019RenewalMail : BaseMail<Families2019RenewalMailView>
|
||||||
|
{
|
||||||
|
public override string Subject { get => "Your Bitwarden Families renewal is updating"; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,584 @@
|
|||||||
|
<!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-100 { width:100% !important; max-width: 100%; }
|
||||||
|
.mj-column-per-70 { width:70% !important; max-width: 70%; }
|
||||||
|
.mj-column-per-30 { width:30% !important; max-width: 30%; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style media="screen and (min-width:480px)">
|
||||||
|
.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }
|
||||||
|
.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%; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
|
||||||
|
@media only screen and (max-width:480px) {
|
||||||
|
.mj-bw-learn-more-footer-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">
|
||||||
|
|
||||||
|
|
||||||
|
<!--[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 20px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:580px;" ><![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 5px;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>
|
||||||
|
|
||||||
|
</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 Section -->
|
||||||
|
|
||||||
|
<!--[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:15px 10px 10px 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 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;">Your Bitwarden Families subscription renews in 15 days. The price is updating to {{BaseMonthlyRenewalPrice}}/month, billed annually
|
||||||
|
at {{BaseAnnualRenewalPrice}} + tax.</div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<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 renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.</div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<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;">Questions? Contact
|
||||||
|
<a href="mailto:support@bitwarden.com" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;">support@bitwarden.com</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><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:0 20px 20px 20px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr></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]-->
|
||||||
|
|
||||||
|
<!-- Learn More Section -->
|
||||||
|
|
||||||
|
<!--[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 10px 20px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#f6f6f6" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
|
||||||
|
|
||||||
|
<div style="background:#f6f6f6;background-color:#f6f6f6;margin:0px auto;border-radius:0px 0px 4px 4px;max-width:620px;">
|
||||||
|
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#f6f6f6;background-color:#f6f6f6;width:100%;border-radius:0px 0px 4px 4px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:5px 10px 10px 10px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:420px;" ><![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;">
|
||||||
|
|
||||||
|
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;"><p style="font-size: 18px; line-height: 28px; font-weight: bold;">
|
||||||
|
Learn more about Bitwarden
|
||||||
|
</p>
|
||||||
|
Find user guides, product documentation, and videos on the
|
||||||
|
<a href="https://bitwarden.com/help/" class="link" style="text-decoration: none; color: #175ddc; font-weight: 600;"> Bitwarden Help Center</a>.</div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--[if mso | IE]></td><td class="" style="vertical-align:top;width:180px;" ><![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:top;width:100%;">
|
||||||
|
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td align="center" class="mj-bw-learn-more-footer-responsive-img" 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:94px;">
|
||||||
|
|
||||||
|
<img alt src="https://assets.bitwarden.com/email/v1/spot-community.png" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;" width="94" 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></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:20px 0;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:660px;" ><![endif]-->
|
||||||
|
|
||||||
|
<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:10px;vertical-align:middle;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||||
|
<a href="https://x.com/bitwarden" target="_blank">
|
||||||
|
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-x.png" style="border-radius:3px;display:block;" width="30">
|
||||||
|
</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:10px;vertical-align:middle;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||||
|
<a href="https://www.reddit.com/r/Bitwarden/" target="_blank">
|
||||||
|
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-reddit.png" style="border-radius:3px;display:block;" width="30">
|
||||||
|
</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:10px;vertical-align:middle;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||||
|
<a href="https://community.bitwarden.com/" target="_blank">
|
||||||
|
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-discourse.png" style="border-radius:3px;display:block;" width="30">
|
||||||
|
</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:10px;vertical-align:middle;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||||
|
<a href="https://github.com/bitwarden" target="_blank">
|
||||||
|
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-github.png" style="border-radius:3px;display:block;" width="30">
|
||||||
|
</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:10px;vertical-align:middle;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||||
|
<a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank">
|
||||||
|
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-youtube.png" style="border-radius:3px;display:block;" width="30">
|
||||||
|
</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:10px;vertical-align:middle;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||||
|
<a href="https://www.linkedin.com/company/bitwarden1/" target="_blank">
|
||||||
|
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-linkedin.png" style="border-radius:3px;display:block;" width="30">
|
||||||
|
</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:10px;vertical-align:middle;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:30px;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="font-size:0;height:30px;vertical-align:middle;width:30px;">
|
||||||
|
<a href="https://www.facebook.com/bitwarden/" target="_blank">
|
||||||
|
<img alt height="30" src="https://assets.bitwarden.com/email/v1/mail-facebook.png" style="border-radius:3px;display:block;" width="30">
|
||||||
|
</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">
|
||||||
|
© 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa
|
||||||
|
Barbara, CA, USA
|
||||||
|
</p>
|
||||||
|
<p style="margin-top: 5px">
|
||||||
|
Always confirm you are on a trusted Bitwarden domain before logging
|
||||||
|
in:<br>
|
||||||
|
<a href="https://bitwarden.com/">bitwarden.com</a> |
|
||||||
|
<a href="https://bitwarden.com/help/emails-from-bitwarden/">Learn why we include this</a>
|
||||||
|
</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 @@
|
|||||||
|
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 renewal will now be billed annually at {{DiscountedAnnualRenewalPrice}} + tax.
|
||||||
|
|
||||||
|
Questions? Contact support@bitwarden.com
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.NotificationCenter.Enums;
|
using Bit.Core.NotificationCenter.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.Models;
|
namespace Bit.Core.Models;
|
||||||
@@ -103,3 +104,9 @@ public class LogOutPushNotification
|
|||||||
public Guid UserId { get; set; }
|
public Guid UserId { get; set; }
|
||||||
public PushNotificationLogOutReason? Reason { get; set; }
|
public PushNotificationLogOutReason? Reason { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class SyncPolicyPushNotification
|
||||||
|
{
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
public required Policy Policy { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -95,5 +95,8 @@ public enum PushType : byte
|
|||||||
OrganizationBankAccountVerified = 23,
|
OrganizationBankAccountVerified = 23,
|
||||||
|
|
||||||
[NotificationInfo("@bitwarden/team-billing-dev", typeof(Models.ProviderBankAccountVerifiedPushNotification))]
|
[NotificationInfo("@bitwarden/team-billing-dev", typeof(Models.ProviderBankAccountVerifiedPushNotification))]
|
||||||
ProviderBankAccountVerified = 24
|
ProviderBankAccountVerified = 24,
|
||||||
|
|
||||||
|
[NotificationInfo("@bitwarden/team-admin-console-dev", typeof(Models.SyncPolicyPushNotification))]
|
||||||
|
PolicyChanged = 25,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
namespace Bit.Identity.IdentityServer.RequestValidationConstants;
|
||||||
|
|
||||||
|
public static class CustomResponseConstants
|
||||||
|
{
|
||||||
|
public static class ResponseKeys
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies the error model returned in the custom response when an error occurs.
|
||||||
|
/// </summary>
|
||||||
|
public static string ErrorModel => "ErrorModel";
|
||||||
|
/// <summary>
|
||||||
|
/// This Key is used when a user is in a single organization that requires SSO authentication. The identifier
|
||||||
|
/// is used by the client to speed the redirection to the correct IdP for the user's organization.
|
||||||
|
/// </summary>
|
||||||
|
public static string SsoOrganizationIdentifier => "SsoOrganizationIdentifier";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SsoConstants
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// These are messages and errors we return when SSO Validation is unsuccessful
|
||||||
|
/// </summary>
|
||||||
|
public static class RequestErrors
|
||||||
|
{
|
||||||
|
public static string SsoRequired => "sso_required";
|
||||||
|
public static string SsoRequiredDescription => "Sso authentication is required.";
|
||||||
|
public static string SsoTwoFactorRecoveryDescription => "Two-factor recovery has been performed. SSO authentication is required.";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IDeviceValidator _deviceValidator;
|
private readonly IDeviceValidator _deviceValidator;
|
||||||
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
||||||
|
private readonly ISsoRequestValidator _ssoRequestValidator;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
@@ -44,7 +45,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
|
|
||||||
protected ICurrentContext CurrentContext { get; }
|
protected ICurrentContext CurrentContext { get; }
|
||||||
protected IPolicyService PolicyService { get; }
|
protected IPolicyService PolicyService { get; }
|
||||||
protected IFeatureService FeatureService { get; }
|
protected IFeatureService _featureService { get; }
|
||||||
protected ISsoConfigRepository SsoConfigRepository { get; }
|
protected ISsoConfigRepository SsoConfigRepository { get; }
|
||||||
protected IUserService _userService { get; }
|
protected IUserService _userService { get; }
|
||||||
protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; }
|
protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; }
|
||||||
@@ -57,6 +58,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
|
ISsoRequestValidator ssoRequestValidator,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@@ -78,13 +80,14 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_deviceValidator = deviceValidator;
|
_deviceValidator = deviceValidator;
|
||||||
_twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;
|
_twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;
|
||||||
|
_ssoRequestValidator = ssoRequestValidator;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
CurrentContext = currentContext;
|
CurrentContext = currentContext;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
PolicyService = policyService;
|
PolicyService = policyService;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
FeatureService = featureService;
|
_featureService = featureService;
|
||||||
SsoConfigRepository = ssoConfigRepository;
|
SsoConfigRepository = ssoConfigRepository;
|
||||||
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
|
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
|
||||||
PolicyRequirementQuery = policyRequirementQuery;
|
PolicyRequirementQuery = policyRequirementQuery;
|
||||||
@@ -97,7 +100,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
||||||
CustomValidatorRequestContext validatorContext)
|
CustomValidatorRequestContext validatorContext)
|
||||||
{
|
{
|
||||||
if (FeatureService.IsEnabled(FeatureFlagKeys.RecoveryCodeSupportForSsoRequiredUsers))
|
if (_featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeSupportForSsoRequiredUsers))
|
||||||
{
|
{
|
||||||
var validators = DetermineValidationOrder(context, request, validatorContext);
|
var validators = DetermineValidationOrder(context, request, validatorContext);
|
||||||
var allValidationSchemesSuccessful = await ProcessValidatorsAsync(validators);
|
var allValidationSchemesSuccessful = await ProcessValidatorsAsync(validators);
|
||||||
@@ -135,15 +138,29 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Decide if this user belongs to an organization that requires SSO.
|
// 2. Decide if this user belongs to an organization that requires SSO.
|
||||||
validatorContext.SsoRequired = await RequireSsoLoginAsync(user, request.GrantType);
|
// TODO: Clean up Feature Flag: Remove this if block: PM-28281
|
||||||
if (validatorContext.SsoRequired)
|
if (!_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired))
|
||||||
{
|
{
|
||||||
SetSsoResult(context,
|
validatorContext.SsoRequired = await RequireSsoLoginAsync(user, request.GrantType);
|
||||||
new Dictionary<string, object>
|
if (validatorContext.SsoRequired)
|
||||||
{
|
{
|
||||||
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
|
SetSsoResult(context,
|
||||||
});
|
new Dictionary<string, object>
|
||||||
return;
|
{
|
||||||
|
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var ssoValid = await _ssoRequestValidator.ValidateAsync(user, request, validatorContext);
|
||||||
|
if (!ssoValid)
|
||||||
|
{
|
||||||
|
// SSO is required
|
||||||
|
SetValidationErrorResult(context, validatorContext);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Check if 2FA is required.
|
// 3. Check if 2FA is required.
|
||||||
@@ -390,36 +407,51 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
private async Task<bool> ValidateSsoAsync(T context, ValidatedTokenRequest request,
|
private async Task<bool> ValidateSsoAsync(T context, ValidatedTokenRequest request,
|
||||||
CustomValidatorRequestContext validatorContext)
|
CustomValidatorRequestContext validatorContext)
|
||||||
{
|
{
|
||||||
validatorContext.SsoRequired = await RequireSsoLoginAsync(validatorContext.User, request.GrantType);
|
// TODO: Clean up Feature Flag: Remove this if block: PM-28281
|
||||||
if (!validatorContext.SsoRequired)
|
if (!_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired))
|
||||||
{
|
{
|
||||||
return true;
|
validatorContext.SsoRequired = await RequireSsoLoginAsync(validatorContext.User, request.GrantType);
|
||||||
}
|
if (!validatorContext.SsoRequired)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Users without SSO requirement requesting 2FA recovery will be fast-forwarded through login and are
|
// Users without SSO requirement requesting 2FA recovery will be fast-forwarded through login and are
|
||||||
// presented with their 2FA management area as a reminder to re-evaluate their 2FA posture after recovery and
|
// presented with their 2FA management area as a reminder to re-evaluate their 2FA posture after recovery and
|
||||||
// review their new recovery token if desired.
|
// review their new recovery token if desired.
|
||||||
// SSO users cannot be assumed to be authenticated, and must prove authentication with their IdP after recovery.
|
// SSO users cannot be assumed to be authenticated, and must prove authentication with their IdP after recovery.
|
||||||
// As described in validation order determination, if TwoFactorRequired, the 2FA validation scheme will have been
|
// As described in validation order determination, if TwoFactorRequired, the 2FA validation scheme will have been
|
||||||
// evaluated, and recovery will have been performed if requested.
|
// evaluated, and recovery will have been performed if requested.
|
||||||
// We will send a descriptive message in these cases so clients can give the appropriate feedback and redirect
|
// We will send a descriptive message in these cases so clients can give the appropriate feedback and redirect
|
||||||
// to /login.
|
// to /login.
|
||||||
if (validatorContext.TwoFactorRequired &&
|
if (validatorContext.TwoFactorRequired &&
|
||||||
validatorContext.TwoFactorRecoveryRequested)
|
validatorContext.TwoFactorRecoveryRequested)
|
||||||
{
|
{
|
||||||
SetSsoResult(context, new Dictionary<string, object>
|
SetSsoResult(context, new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
{ "ErrorModel", new ErrorResponseModel("Two-factor recovery has been performed. SSO authentication is required.") }
|
{ "ErrorModel", new ErrorResponseModel("Two-factor recovery has been performed. SSO authentication is required.") }
|
||||||
});
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetSsoResult(context,
|
||||||
|
new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
|
||||||
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
SetSsoResult(context,
|
{
|
||||||
new Dictionary<string, object>
|
var ssoValid = await _ssoRequestValidator.ValidateAsync(validatorContext.User, request, validatorContext);
|
||||||
|
if (ssoValid)
|
||||||
{
|
{
|
||||||
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
|
return true;
|
||||||
});
|
}
|
||||||
return false;
|
|
||||||
|
SetValidationErrorResult(context, validatorContext);
|
||||||
|
return ssoValid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -686,6 +718,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
/// <param name="user">user trying to login</param>
|
/// <param name="user">user trying to login</param>
|
||||||
/// <param name="grantType">magic string identifying the grant type requested</param>
|
/// <param name="grantType">magic string identifying the grant type requested</param>
|
||||||
/// <returns>true if sso required; false if not required or already in process</returns>
|
/// <returns>true if sso required; false if not required or already in process</returns>
|
||||||
|
[Obsolete("This method is deprecated and will be removed in future versions, PM-28281. Please use the SsoRequestValidator scheme instead.")]
|
||||||
private async Task<bool> RequireSsoLoginAsync(User user, string grantType)
|
private async Task<bool> RequireSsoLoginAsync(User user, string grantType)
|
||||||
{
|
{
|
||||||
if (grantType == "authorization_code" || grantType == "client_credentials")
|
if (grantType == "authorization_code" || grantType == "client_credentials")
|
||||||
@@ -696,7 +729,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if user belongs to any organization with an active SSO policy
|
// Check if user belongs to any organization with an active SSO policy
|
||||||
var ssoRequired = FeatureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
var ssoRequired = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||||
? (await PolicyRequirementQuery.GetAsync<RequireSsoPolicyRequirement>(user.Id))
|
? (await PolicyRequirementQuery.GetAsync<RequireSsoPolicyRequirement>(user.Id))
|
||||||
.SsoRequired
|
.SsoRequired
|
||||||
: await PolicyService.AnyPoliciesApplicableToUserAsync(
|
: await PolicyService.AnyPoliciesApplicableToUserAsync(
|
||||||
@@ -738,7 +771,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
|
|
||||||
private async Task SendFailedTwoFactorEmail(User user, TwoFactorProviderType failedAttemptType)
|
private async Task SendFailedTwoFactorEmail(User user, TwoFactorProviderType failedAttemptType)
|
||||||
{
|
{
|
||||||
if (FeatureService.IsEnabled(FeatureFlagKeys.FailedTwoFactorEmail))
|
if (_featureService.IsEnabled(FeatureFlagKeys.FailedTwoFactorEmail))
|
||||||
{
|
{
|
||||||
await _mailService.SendFailedTwoFactorAttemptEmailAsync(user.Email, failedAttemptType, DateTime.UtcNow,
|
await _mailService.SendFailedTwoFactorAttemptEmailAsync(user.Email, failedAttemptType, DateTime.UtcNow,
|
||||||
CurrentContext.IpAddress);
|
CurrentContext.IpAddress);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
|||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
|
ISsoRequestValidator ssoRequestValidator,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
ILogger<CustomTokenRequestValidator> logger,
|
ILogger<CustomTokenRequestValidator> logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@@ -54,6 +55,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
|||||||
eventService,
|
eventService,
|
||||||
deviceValidator,
|
deviceValidator,
|
||||||
twoFactorAuthenticationValidator,
|
twoFactorAuthenticationValidator,
|
||||||
|
ssoRequestValidator,
|
||||||
organizationUserRepository,
|
organizationUserRepository,
|
||||||
logger,
|
logger,
|
||||||
currentContext,
|
currentContext,
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Duende.IdentityServer.Validation;
|
||||||
|
|
||||||
|
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates whether a user is required to authenticate via SSO based on organization policies.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISsoRequestValidator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the SSO requirement for a user attempting to authenticate. Sets the error state in the <see cref="CustomValidatorRequestContext.CustomResponse"/> if SSO is required.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user attempting to authenticate.</param>
|
||||||
|
/// <param name="request">The token request containing grant type and other authentication details.</param>
|
||||||
|
/// <param name="context">The validator context to be updated with SSO requirement status and error results if applicable.</param>
|
||||||
|
/// <returns>true if the user can proceed with authentication; false if SSO is required and the user must be redirected to SSO flow.</returns>
|
||||||
|
Task<bool> ValidateAsync(User user, ValidatedTokenRequest request, CustomValidatorRequestContext context);
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
|||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
|
ISsoRequestValidator ssoRequestValidator,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
ILogger<ResourceOwnerPasswordValidator> logger,
|
ILogger<ResourceOwnerPasswordValidator> logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@@ -51,6 +52,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
|||||||
eventService,
|
eventService,
|
||||||
deviceValidator,
|
deviceValidator,
|
||||||
twoFactorAuthenticationValidator,
|
twoFactorAuthenticationValidator,
|
||||||
|
ssoRequestValidator,
|
||||||
organizationUserRepository,
|
organizationUserRepository,
|
||||||
logger,
|
logger,
|
||||||
currentContext,
|
currentContext,
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Auth.Sso;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Api;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Identity.IdentityServer.RequestValidationConstants;
|
||||||
|
using Duende.IdentityModel;
|
||||||
|
using Duende.IdentityServer.Validation;
|
||||||
|
|
||||||
|
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates whether a user is required to authenticate via SSO based on organization policies.
|
||||||
|
/// </summary>
|
||||||
|
public class SsoRequestValidator(
|
||||||
|
IPolicyService _policyService,
|
||||||
|
IFeatureService _featureService,
|
||||||
|
IUserSsoOrganizationIdentifierQuery _userSsoOrganizationIdentifierQuery,
|
||||||
|
IPolicyRequirementQuery _policyRequirementQuery) : ISsoRequestValidator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the SSO requirement for a user attempting to authenticate.
|
||||||
|
/// Sets context.SsoRequired to indicate whether SSO is required.
|
||||||
|
/// If SSO is required, sets the validation error result and custom response in the context.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user attempting to authenticate.</param>
|
||||||
|
/// <param name="request">The token request containing grant type and other authentication details.</param>
|
||||||
|
/// <param name="context">The validator context to be updated with SSO requirement status and error results if applicable.</param>
|
||||||
|
/// <returns>true if the user can proceed with authentication; false if SSO is required and the user must be redirected to SSO flow.</returns>
|
||||||
|
public async Task<bool> ValidateAsync(User user, ValidatedTokenRequest request, CustomValidatorRequestContext context)
|
||||||
|
{
|
||||||
|
context.SsoRequired = await RequireSsoAuthenticationAsync(user, request.GrantType);
|
||||||
|
|
||||||
|
if (!context.SsoRequired)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users without SSO requirement requesting 2FA recovery will be fast-forwarded through login and are
|
||||||
|
// presented with their 2FA management area as a reminder to re-evaluate their 2FA posture after recovery and
|
||||||
|
// review their new recovery token if desired.
|
||||||
|
// SSO users cannot be assumed to be authenticated, and must prove authentication with their IdP after recovery.
|
||||||
|
// As described in validation order determination, if TwoFactorRequired, the 2FA validation scheme will have been
|
||||||
|
// evaluated, and recovery will have been performed if requested.
|
||||||
|
// We will send a descriptive message in these cases so clients can give the appropriate feedback and redirect
|
||||||
|
// to /login.
|
||||||
|
// If the feature flag RecoveryCodeSupportForSsoRequiredUsers is set to false then this code is unreachable since
|
||||||
|
// Two Factor validation occurs after SSO validation in that scenario.
|
||||||
|
if (context.TwoFactorRequired && context.TwoFactorRecoveryRequested)
|
||||||
|
{
|
||||||
|
await SetContextCustomResponseSsoErrorAsync(context, SsoConstants.RequestErrors.SsoTwoFactorRecoveryDescription);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SetContextCustomResponseSsoErrorAsync(context, SsoConstants.RequestErrors.SsoRequiredDescription);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the user is required to authenticate via SSO. If the user requires SSO, but they are
|
||||||
|
/// logging in using an API Key (client_credentials) then they are allowed to bypass the SSO requirement.
|
||||||
|
/// If the GrantType is authorization_code or client_credentials we know the user is trying to login
|
||||||
|
/// using the SSO flow so they are allowed to continue.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">user trying to login</param>
|
||||||
|
/// <param name="grantType">magic string identifying the grant type requested</param>
|
||||||
|
/// <returns>true if sso required; false if not required or already in process</returns>
|
||||||
|
private async Task<bool> RequireSsoAuthenticationAsync(User user, string grantType)
|
||||||
|
{
|
||||||
|
if (grantType == OidcConstants.GrantTypes.AuthorizationCode ||
|
||||||
|
grantType == OidcConstants.GrantTypes.ClientCredentials)
|
||||||
|
{
|
||||||
|
// SSO is not required for users already using SSO to authenticate which uses the authorization_code grant type,
|
||||||
|
// or logging-in via API key which is the client_credentials grant type.
|
||||||
|
// Allow user to continue request validation
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user belongs to any organization with an active SSO policy
|
||||||
|
var ssoRequired = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||||
|
? (await _policyRequirementQuery.GetAsync<RequireSsoPolicyRequirement>(user.Id))
|
||||||
|
.SsoRequired
|
||||||
|
: await _policyService.AnyPoliciesApplicableToUserAsync(
|
||||||
|
user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
||||||
|
|
||||||
|
if (ssoRequired)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default - SSO is not required
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the customResponse in the context with the error result for the SSO validation failure.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The validator context to update with error details.</param>
|
||||||
|
/// <param name="errorMessage">The error message to return to the client.</param>
|
||||||
|
private async Task SetContextCustomResponseSsoErrorAsync(CustomValidatorRequestContext context, string errorMessage)
|
||||||
|
{
|
||||||
|
var ssoOrganizationIdentifier = await _userSsoOrganizationIdentifierQuery.GetSsoOrganizationIdentifierAsync(context.User.Id);
|
||||||
|
|
||||||
|
context.ValidationErrorResult = new ValidationResult
|
||||||
|
{
|
||||||
|
IsError = true,
|
||||||
|
Error = OidcConstants.TokenErrors.InvalidGrant,
|
||||||
|
ErrorDescription = errorMessage
|
||||||
|
};
|
||||||
|
|
||||||
|
context.CustomResponse = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ CustomResponseConstants.ResponseKeys.ErrorModel, new ErrorResponseModel(errorMessage) }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Include organization identifier in the response if available
|
||||||
|
if (!string.IsNullOrEmpty(ssoOrganizationIdentifier))
|
||||||
|
{
|
||||||
|
context.CustomResponse[CustomResponseConstants.ResponseKeys.SsoOrganizationIdentifier] = ssoOrganizationIdentifier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
|||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
|
ISsoRequestValidator ssoRequestValidator,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
ILogger<CustomTokenRequestValidator> logger,
|
ILogger<CustomTokenRequestValidator> logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@@ -60,6 +61,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
|||||||
eventService,
|
eventService,
|
||||||
deviceValidator,
|
deviceValidator,
|
||||||
twoFactorAuthenticationValidator,
|
twoFactorAuthenticationValidator,
|
||||||
|
ssoRequestValidator,
|
||||||
organizationUserRepository,
|
organizationUserRepository,
|
||||||
logger,
|
logger,
|
||||||
currentContext,
|
currentContext,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddTransient<IDeviceValidator, DeviceValidator>();
|
services.AddTransient<IDeviceValidator, DeviceValidator>();
|
||||||
services.AddTransient<IClientVersionValidator, ClientVersionValidator>();
|
services.AddTransient<IClientVersionValidator, ClientVersionValidator>();
|
||||||
services.AddTransient<ITwoFactorAuthenticationValidator, TwoFactorAuthenticationValidator>();
|
services.AddTransient<ITwoFactorAuthenticationValidator, TwoFactorAuthenticationValidator>();
|
||||||
|
services.AddTransient<ISsoRequestValidator, SsoRequestValidator>();
|
||||||
services.AddTransient<ILoginApprovingClientTypes, LoginApprovingClientTypes>();
|
services.AddTransient<ILoginApprovingClientTypes, LoginApprovingClientTypes>();
|
||||||
services.AddTransient<ISendAuthenticationMethodValidator<ResourcePassword>, SendPasswordRequestValidator>();
|
services.AddTransient<ISendAuthenticationMethodValidator<ResourcePassword>, SendPasswordRequestValidator>();
|
||||||
services.AddTransient<ISendAuthenticationMethodValidator<EmailOtp>, SendEmailOtpRequestValidator>();
|
services.AddTransient<ISendAuthenticationMethodValidator<EmailOtp>, SendEmailOtpRequestValidator>();
|
||||||
|
|||||||
@@ -113,7 +113,8 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
|||||||
UseRiskInsights = e.UseRiskInsights,
|
UseRiskInsights = e.UseRiskInsights,
|
||||||
UseOrganizationDomains = e.UseOrganizationDomains,
|
UseOrganizationDomains = e.UseOrganizationDomains,
|
||||||
UseAdminSponsoredFamilies = e.UseAdminSponsoredFamilies,
|
UseAdminSponsoredFamilies = e.UseAdminSponsoredFamilies,
|
||||||
UseAutomaticUserConfirmation = e.UseAutomaticUserConfirmation
|
UseAutomaticUserConfirmation = e.UseAutomaticUserConfirmation,
|
||||||
|
UsePhishingBlocker = e.UsePhishingBlocker
|
||||||
}).ToListAsync();
|
}).ToListAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
|
|||||||
LimitItemDeletion = o.LimitItemDeletion,
|
LimitItemDeletion = o.LimitItemDeletion,
|
||||||
IsAdminInitiated = os.IsAdminInitiated,
|
IsAdminInitiated = os.IsAdminInitiated,
|
||||||
UseOrganizationDomains = o.UseOrganizationDomains,
|
UseOrganizationDomains = o.UseOrganizationDomains,
|
||||||
UseAutomaticUserConfirmation = o.UseAutomaticUserConfirmation
|
UseAutomaticUserConfirmation = o.UseAutomaticUserConfirmation,
|
||||||
|
UsePhishingBlocker = o.UsePhishingBlocker
|
||||||
};
|
};
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ public class ProviderUserOrganizationDetailsViewQuery : IQuery<ProviderUserOrgan
|
|||||||
UseAutomaticUserConfirmation = x.o.UseAutomaticUserConfirmation,
|
UseAutomaticUserConfirmation = x.o.UseAutomaticUserConfirmation,
|
||||||
SsoEnabled = x.ss.Enabled,
|
SsoEnabled = x.ss.Enabled,
|
||||||
SsoConfig = x.ss.Data,
|
SsoConfig = x.ss.Data,
|
||||||
|
UsePhishingBlocker = x.o.UsePhishingBlocker
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,9 +231,26 @@ public class HubHelpers
|
|||||||
await _hubContext.Clients.User(pendingTasksData.Payload.UserId.ToString())
|
await _hubContext.Clients.User(pendingTasksData.Payload.UserId.ToString())
|
||||||
.SendAsync(_receiveMessageMethod, pendingTasksData, cancellationToken);
|
.SendAsync(_receiveMessageMethod, pendingTasksData, cancellationToken);
|
||||||
break;
|
break;
|
||||||
|
case PushType.PolicyChanged:
|
||||||
|
await policyChangedNotificationHandler(notificationJson, cancellationToken);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
_logger.LogWarning("Notification type '{NotificationType}' has not been registered in HubHelpers and will not be pushed as as result", notification.Type);
|
_logger.LogWarning("Notification type '{NotificationType}' has not been registered in HubHelpers and will not be pushed as as result", notification.Type);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task policyChangedNotificationHandler(string notificationJson, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var policyData = JsonSerializer.Deserialize<PushNotificationData<SyncPolicyPushNotification>>(notificationJson, _deserializerOptions);
|
||||||
|
if (policyData is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _hubContext.Clients
|
||||||
|
.Group(NotificationsHub.GetOrganizationGroup(policyData.Payload.OrganizationId))
|
||||||
|
.SendAsync(_receiveMessageMethod, policyData, cancellationToken);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ CREATE PROCEDURE [dbo].[Organization_Create]
|
|||||||
@UseOrganizationDomains BIT = 0,
|
@UseOrganizationDomains BIT = 0,
|
||||||
@UseAdminSponsoredFamilies BIT = 0,
|
@UseAdminSponsoredFamilies BIT = 0,
|
||||||
@SyncSeats BIT = 0,
|
@SyncSeats BIT = 0,
|
||||||
@UseAutomaticUserConfirmation BIT = 0
|
@UseAutomaticUserConfirmation BIT = 0,
|
||||||
|
@UsePhishingBlocker BIT = 0
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@@ -126,7 +127,8 @@ BEGIN
|
|||||||
[UseOrganizationDomains],
|
[UseOrganizationDomains],
|
||||||
[UseAdminSponsoredFamilies],
|
[UseAdminSponsoredFamilies],
|
||||||
[SyncSeats],
|
[SyncSeats],
|
||||||
[UseAutomaticUserConfirmation]
|
[UseAutomaticUserConfirmation],
|
||||||
|
[UsePhishingBlocker]
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
@@ -190,6 +192,7 @@ BEGIN
|
|||||||
@UseOrganizationDomains,
|
@UseOrganizationDomains,
|
||||||
@UseAdminSponsoredFamilies,
|
@UseAdminSponsoredFamilies,
|
||||||
@SyncSeats,
|
@SyncSeats,
|
||||||
@UseAutomaticUserConfirmation
|
@UseAutomaticUserConfirmation,
|
||||||
|
@UsePhishingBlocker
|
||||||
);
|
);
|
||||||
END
|
END
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ BEGIN
|
|||||||
[LimitItemDeletion],
|
[LimitItemDeletion],
|
||||||
[UseOrganizationDomains],
|
[UseOrganizationDomains],
|
||||||
[UseAdminSponsoredFamilies],
|
[UseAdminSponsoredFamilies],
|
||||||
[UseAutomaticUserConfirmation]
|
[UseAutomaticUserConfirmation],
|
||||||
|
[UsePhishingBlocker]
|
||||||
FROM
|
FROM
|
||||||
[dbo].[Organization]
|
[dbo].[Organization]
|
||||||
END
|
END
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ CREATE PROCEDURE [dbo].[Organization_Update]
|
|||||||
@UseOrganizationDomains BIT = 0,
|
@UseOrganizationDomains BIT = 0,
|
||||||
@UseAdminSponsoredFamilies BIT = 0,
|
@UseAdminSponsoredFamilies BIT = 0,
|
||||||
@SyncSeats BIT = 0,
|
@SyncSeats BIT = 0,
|
||||||
@UseAutomaticUserConfirmation BIT = 0
|
@UseAutomaticUserConfirmation BIT = 0,
|
||||||
|
@UsePhishingBlocker BIT = 0
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@@ -126,7 +127,8 @@ BEGIN
|
|||||||
[UseOrganizationDomains] = @UseOrganizationDomains,
|
[UseOrganizationDomains] = @UseOrganizationDomains,
|
||||||
[UseAdminSponsoredFamilies] = @UseAdminSponsoredFamilies,
|
[UseAdminSponsoredFamilies] = @UseAdminSponsoredFamilies,
|
||||||
[SyncSeats] = @SyncSeats,
|
[SyncSeats] = @SyncSeats,
|
||||||
[UseAutomaticUserConfirmation] = @UseAutomaticUserConfirmation
|
[UseAutomaticUserConfirmation] = @UseAutomaticUserConfirmation,
|
||||||
|
[UsePhishingBlocker] = @UsePhishingBlocker
|
||||||
WHERE
|
WHERE
|
||||||
[Id] = @Id;
|
[Id] = @Id;
|
||||||
END
|
END
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ CREATE TABLE [dbo].[Organization] (
|
|||||||
[SyncSeats] BIT NOT NULL CONSTRAINT [DF_Organization_SyncSeats] DEFAULT (0),
|
[SyncSeats] BIT NOT NULL CONSTRAINT [DF_Organization_SyncSeats] DEFAULT (0),
|
||||||
[UseAutomaticUserConfirmation] BIT NOT NULL CONSTRAINT [DF_Organization_UseAutomaticUserConfirmation] DEFAULT (0),
|
[UseAutomaticUserConfirmation] BIT NOT NULL CONSTRAINT [DF_Organization_UseAutomaticUserConfirmation] DEFAULT (0),
|
||||||
[MaxStorageGbIncreased] SMALLINT NULL,
|
[MaxStorageGbIncreased] SMALLINT NULL,
|
||||||
|
[UsePhishingBlocker] BIT NOT NULL CONSTRAINT [DF_Organization_UsePhishingBlocker] DEFAULT (0),
|
||||||
CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC)
|
CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ SELECT
|
|||||||
O.[UseAdminSponsoredFamilies],
|
O.[UseAdminSponsoredFamilies],
|
||||||
O.[UseOrganizationDomains],
|
O.[UseOrganizationDomains],
|
||||||
OS.[IsAdminInitiated],
|
OS.[IsAdminInitiated],
|
||||||
O.[UseAutomaticUserConfirmation]
|
O.[UseAutomaticUserConfirmation],
|
||||||
|
O.[UsePhishingBlocker]
|
||||||
FROM
|
FROM
|
||||||
[dbo].[OrganizationUser] OU
|
[dbo].[OrganizationUser] OU
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ SELECT
|
|||||||
[UseOrganizationDomains],
|
[UseOrganizationDomains],
|
||||||
[UseAdminSponsoredFamilies],
|
[UseAdminSponsoredFamilies],
|
||||||
[SyncSeats],
|
[SyncSeats],
|
||||||
[UseAutomaticUserConfirmation]
|
[UseAutomaticUserConfirmation],
|
||||||
|
[UsePhishingBlocker]
|
||||||
FROM
|
FROM
|
||||||
[dbo].[Organization]
|
[dbo].[Organization]
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ SELECT
|
|||||||
O.[UseOrganizationDomains],
|
O.[UseOrganizationDomains],
|
||||||
O.[UseAutomaticUserConfirmation],
|
O.[UseAutomaticUserConfirmation],
|
||||||
SS.[Enabled] SsoEnabled,
|
SS.[Enabled] SsoEnabled,
|
||||||
SS.[Data] SsoConfig
|
SS.[Data] SsoConfig,
|
||||||
|
O.[UsePhishingBlocker]
|
||||||
FROM
|
FROM
|
||||||
[dbo].[ProviderUser] PU
|
[dbo].[ProviderUser] PU
|
||||||
INNER JOIN
|
INNER JOIN
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ public class ProfileOrganizationResponseModelTests
|
|||||||
UsersGetPremium = organization.UsersGetPremium,
|
UsersGetPremium = organization.UsersGetPremium,
|
||||||
UseCustomPermissions = organization.UseCustomPermissions,
|
UseCustomPermissions = organization.UseCustomPermissions,
|
||||||
UseRiskInsights = organization.UseRiskInsights,
|
UseRiskInsights = organization.UseRiskInsights,
|
||||||
|
UsePhishingBlocker = organization.UsePhishingBlocker,
|
||||||
UseOrganizationDomains = organization.UseOrganizationDomains,
|
UseOrganizationDomains = organization.UseOrganizationDomains,
|
||||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies,
|
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies,
|
||||||
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation,
|
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation,
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public class ProfileProviderOrganizationResponseModelTests
|
|||||||
UsersGetPremium = organization.UsersGetPremium,
|
UsersGetPremium = organization.UsersGetPremium,
|
||||||
UseCustomPermissions = organization.UseCustomPermissions,
|
UseCustomPermissions = organization.UseCustomPermissions,
|
||||||
UseRiskInsights = organization.UseRiskInsights,
|
UseRiskInsights = organization.UseRiskInsights,
|
||||||
|
UsePhishingBlocker = organization.UsePhishingBlocker,
|
||||||
UseOrganizationDomains = organization.UseOrganizationDomains,
|
UseOrganizationDomains = organization.UseOrganizationDomains,
|
||||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies,
|
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies,
|
||||||
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation,
|
UseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation,
|
||||||
|
|||||||
292
test/Api.Test/Dirt/HibpControllerTests.cs
Normal file
292
test/Api.Test/Dirt/HibpControllerTests.cs
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Reflection;
|
||||||
|
using Bit.Api.Dirt.Controllers;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||||
|
|
||||||
|
namespace Bit.Api.Test.Dirt;
|
||||||
|
|
||||||
|
[ControllerCustomize(typeof(HibpController))]
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class HibpControllerTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly HttpClient _originalHttpClient;
|
||||||
|
private readonly FieldInfo _httpClientField;
|
||||||
|
|
||||||
|
public HibpControllerTests()
|
||||||
|
{
|
||||||
|
// Store original HttpClient for restoration
|
||||||
|
_httpClientField = typeof(HibpController).GetField("_httpClient", BindingFlags.Static | BindingFlags.NonPublic);
|
||||||
|
_originalHttpClient = (HttpClient)_httpClientField?.GetValue(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// Restore original HttpClient after tests
|
||||||
|
_httpClientField?.SetValue(null, _originalHttpClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Get_WithMissingApiKey_ThrowsBadRequestException(
|
||||||
|
SutProvider<HibpController> sutProvider,
|
||||||
|
string username)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<GlobalSettings>().HibpApiKey = null;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
async () => await sutProvider.Sut.Get(username));
|
||||||
|
Assert.Equal("HaveIBeenPwned API key not set.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Get_WithValidApiKeyAndNoBreaches_Returns200WithEmptyArray(
|
||||||
|
SutProvider<HibpController> sutProvider,
|
||||||
|
string username,
|
||||||
|
Guid userId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<GlobalSettings>().HibpApiKey = "test-api-key";
|
||||||
|
var user = new User { Id = userId };
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>())
|
||||||
|
.Returns(userId);
|
||||||
|
|
||||||
|
// Mock HttpClient to return 404 (no breaches found)
|
||||||
|
var mockHttpClient = CreateMockHttpClient(HttpStatusCode.NotFound, "");
|
||||||
|
_httpClientField.SetValue(null, mockHttpClient);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.Get(username);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var contentResult = Assert.IsType<ContentResult>(result);
|
||||||
|
Assert.Equal("[]", contentResult.Content);
|
||||||
|
Assert.Equal("application/json", contentResult.ContentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Get_WithValidApiKeyAndBreachesFound_Returns200WithBreachData(
|
||||||
|
SutProvider<HibpController> sutProvider,
|
||||||
|
string username,
|
||||||
|
Guid userId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<GlobalSettings>().HibpApiKey = "test-api-key";
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>())
|
||||||
|
.Returns(userId);
|
||||||
|
|
||||||
|
var breachData = "[{\"Name\":\"Adobe\",\"Title\":\"Adobe\",\"Domain\":\"adobe.com\"}]";
|
||||||
|
var mockHttpClient = CreateMockHttpClient(HttpStatusCode.OK, breachData);
|
||||||
|
_httpClientField.SetValue(null, mockHttpClient);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.Get(username);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var contentResult = Assert.IsType<ContentResult>(result);
|
||||||
|
Assert.Equal(breachData, contentResult.Content);
|
||||||
|
Assert.Equal("application/json", contentResult.ContentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Get_WithRateLimiting_RetriesWithDelay(
|
||||||
|
SutProvider<HibpController> sutProvider,
|
||||||
|
string username,
|
||||||
|
Guid userId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<GlobalSettings>().HibpApiKey = "test-api-key";
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>())
|
||||||
|
.Returns(userId);
|
||||||
|
|
||||||
|
// First response is rate limited, second is success
|
||||||
|
var requestCount = 0;
|
||||||
|
var mockHandler = new MockHttpMessageHandler((request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
requestCount++;
|
||||||
|
if (requestCount == 1)
|
||||||
|
{
|
||||||
|
var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests);
|
||||||
|
response.Headers.Add("retry-after", "1");
|
||||||
|
return Task.FromResult(response);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
Content = new StringContent("")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var mockHttpClient = new HttpClient(mockHandler);
|
||||||
|
_httpClientField.SetValue(null, mockHttpClient);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.Get(username);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, requestCount); // Verify retry happened
|
||||||
|
var contentResult = Assert.IsType<ContentResult>(result);
|
||||||
|
Assert.Equal("[]", contentResult.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Get_WithServerError_ThrowsBadRequestException(
|
||||||
|
SutProvider<HibpController> sutProvider,
|
||||||
|
string username,
|
||||||
|
Guid userId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<GlobalSettings>().HibpApiKey = "test-api-key";
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>())
|
||||||
|
.Returns(userId);
|
||||||
|
|
||||||
|
var mockHttpClient = CreateMockHttpClient(HttpStatusCode.InternalServerError, "");
|
||||||
|
_httpClientField.SetValue(null, mockHttpClient);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
async () => await sutProvider.Sut.Get(username));
|
||||||
|
Assert.Contains("Request failed. Status code:", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Get_WithBadRequest_ThrowsBadRequestException(
|
||||||
|
SutProvider<HibpController> sutProvider,
|
||||||
|
string username,
|
||||||
|
Guid userId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<GlobalSettings>().HibpApiKey = "test-api-key";
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>())
|
||||||
|
.Returns(userId);
|
||||||
|
|
||||||
|
var mockHttpClient = CreateMockHttpClient(HttpStatusCode.BadRequest, "");
|
||||||
|
_httpClientField.SetValue(null, mockHttpClient);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
async () => await sutProvider.Sut.Get(username));
|
||||||
|
Assert.Contains("Request failed. Status code:", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Get_EncodesUsernameCorrectly(
|
||||||
|
SutProvider<HibpController> sutProvider,
|
||||||
|
Guid userId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var usernameWithSpecialChars = "test+user@example.com";
|
||||||
|
sutProvider.GetDependency<GlobalSettings>().HibpApiKey = "test-api-key";
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>())
|
||||||
|
.Returns(userId);
|
||||||
|
|
||||||
|
string capturedUrl = null;
|
||||||
|
var mockHandler = new MockHttpMessageHandler((request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
capturedUrl = request.RequestUri.ToString();
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
Content = new StringContent("")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var mockHttpClient = new HttpClient(mockHandler);
|
||||||
|
_httpClientField.SetValue(null, mockHttpClient);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.Get(usernameWithSpecialChars);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(capturedUrl);
|
||||||
|
// Username should be URL encoded (+ becomes %2B, @ becomes %40)
|
||||||
|
Assert.Contains("test%2Buser%40example.com", capturedUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SendAsync_IncludesRequiredHeaders(
|
||||||
|
SutProvider<HibpController> sutProvider,
|
||||||
|
string username,
|
||||||
|
Guid userId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<GlobalSettings>().HibpApiKey = "test-api-key";
|
||||||
|
sutProvider.GetDependency<GlobalSettings>().SelfHosted = false;
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.GetProperUserId(Arg.Any<System.Security.Claims.ClaimsPrincipal>())
|
||||||
|
.Returns(userId);
|
||||||
|
|
||||||
|
HttpRequestMessage capturedRequest = null;
|
||||||
|
var mockHandler = new MockHttpMessageHandler((request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
capturedRequest = request;
|
||||||
|
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
|
||||||
|
{
|
||||||
|
Content = new StringContent("")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var mockHttpClient = new HttpClient(mockHandler);
|
||||||
|
_httpClientField.SetValue(null, mockHttpClient);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.Get(username);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(capturedRequest);
|
||||||
|
Assert.True(capturedRequest.Headers.Contains("hibp-api-key"));
|
||||||
|
Assert.True(capturedRequest.Headers.Contains("hibp-client-id"));
|
||||||
|
Assert.True(capturedRequest.Headers.Contains("User-Agent"));
|
||||||
|
Assert.Equal("Bitwarden", capturedRequest.Headers.GetValues("User-Agent").First());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper to create a mock HttpClient that returns a specific status code and content
|
||||||
|
/// </summary>
|
||||||
|
private HttpClient CreateMockHttpClient(HttpStatusCode statusCode, string content)
|
||||||
|
{
|
||||||
|
var mockHandler = new MockHttpMessageHandler((request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
return Task.FromResult(new HttpResponseMessage(statusCode)
|
||||||
|
{
|
||||||
|
Content = new StringContent(content)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return new HttpClient(mockHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mock HttpMessageHandler for testing HttpClient behavior
|
||||||
|
/// </summary>
|
||||||
|
public class MockHttpMessageHandler : HttpMessageHandler
|
||||||
|
{
|
||||||
|
private readonly Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> _sendAsync;
|
||||||
|
|
||||||
|
public MockHttpMessageHandler(Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> sendAsync)
|
||||||
|
{
|
||||||
|
_sendAsync = sendAsync;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return _sendAsync(request, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ using Bit.Core.Billing.Payment.Queries;
|
|||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Pricing.Premium;
|
using Bit.Core.Billing.Pricing.Premium;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Models.Mail.Billing.Renewal.Families2019Renewal;
|
||||||
using Bit.Core.Models.Mail.Billing.Renewal.Families2020Renewal;
|
using Bit.Core.Models.Mail.Billing.Renewal.Families2020Renewal;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
using Bit.Core.Platform.Mail.Mailer;
|
using Bit.Core.Platform.Mail.Mailer;
|
||||||
@@ -1006,8 +1007,11 @@ public class UpcomingInvoiceHandlerTests
|
|||||||
PlanType = PlanType.FamiliesAnnually2019
|
PlanType = PlanType.FamiliesAnnually2019
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var coupon = new Coupon { PercentOff = 25, Id = CouponIDs.Milestone3SubscriptionDiscount };
|
||||||
|
|
||||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||||
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
|
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
|
||||||
|
_stripeFacade.GetCoupon(CouponIDs.Milestone3SubscriptionDiscount).Returns(coupon);
|
||||||
_stripeEventUtilityService
|
_stripeEventUtilityService
|
||||||
.GetIdsFromMetadata(subscription.Metadata)
|
.GetIdsFromMetadata(subscription.Metadata)
|
||||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
|
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
|
||||||
@@ -1033,6 +1037,8 @@ public class UpcomingInvoiceHandlerTests
|
|||||||
o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&
|
o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&
|
||||||
o.ProrationBehavior == ProrationBehavior.None));
|
o.ProrationBehavior == ProrationBehavior.None));
|
||||||
|
|
||||||
|
await _stripeFacade.Received(1).GetCoupon(CouponIDs.Milestone3SubscriptionDiscount);
|
||||||
|
|
||||||
await _organizationRepository.Received(1).ReplaceAsync(
|
await _organizationRepository.Received(1).ReplaceAsync(
|
||||||
Arg.Is<Organization>(org =>
|
Arg.Is<Organization>(org =>
|
||||||
org.Id == _organizationId &&
|
org.Id == _organizationId &&
|
||||||
@@ -1042,10 +1048,13 @@ public class UpcomingInvoiceHandlerTests
|
|||||||
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
||||||
|
|
||||||
await _mailer.Received(1).SendEmail(
|
await _mailer.Received(1).SendEmail(
|
||||||
Arg.Is<Families2020RenewalMail>(email =>
|
Arg.Is<Families2019RenewalMail>(email =>
|
||||||
email.ToEmails.Contains("org@example.com") &&
|
email.ToEmails.Contains("org@example.com") &&
|
||||||
email.Subject == "Your Bitwarden Families renewal is updating" &&
|
email.Subject == "Your Bitwarden Families renewal is updating" &&
|
||||||
email.View.MonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US"))));
|
email.View.BaseMonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US")) &&
|
||||||
|
email.View.BaseAnnualRenewalPrice == familiesPlan.PasswordManager.BasePrice.ToString("C", new CultureInfo("en-US")) &&
|
||||||
|
email.View.DiscountAmount == $"{coupon.PercentOff}%"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -1529,8 +1538,11 @@ public class UpcomingInvoiceHandlerTests
|
|||||||
PlanType = PlanType.FamiliesAnnually2019
|
PlanType = PlanType.FamiliesAnnually2019
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var coupon = new Coupon { PercentOff = 25, Id = CouponIDs.Milestone3SubscriptionDiscount };
|
||||||
|
|
||||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||||
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
|
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
|
||||||
|
_stripeFacade.GetCoupon(CouponIDs.Milestone3SubscriptionDiscount).Returns(coupon);
|
||||||
_stripeEventUtilityService
|
_stripeEventUtilityService
|
||||||
.GetIdsFromMetadata(subscription.Metadata)
|
.GetIdsFromMetadata(subscription.Metadata)
|
||||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
|
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
|
||||||
@@ -1556,6 +1568,8 @@ public class UpcomingInvoiceHandlerTests
|
|||||||
o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&
|
o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&
|
||||||
o.ProrationBehavior == ProrationBehavior.None));
|
o.ProrationBehavior == ProrationBehavior.None));
|
||||||
|
|
||||||
|
await _stripeFacade.Received(1).GetCoupon(CouponIDs.Milestone3SubscriptionDiscount);
|
||||||
|
|
||||||
await _organizationRepository.Received(1).ReplaceAsync(
|
await _organizationRepository.Received(1).ReplaceAsync(
|
||||||
Arg.Is<Organization>(org =>
|
Arg.Is<Organization>(org =>
|
||||||
org.Id == _organizationId &&
|
org.Id == _organizationId &&
|
||||||
@@ -1565,10 +1579,13 @@ public class UpcomingInvoiceHandlerTests
|
|||||||
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
||||||
|
|
||||||
await _mailer.Received(1).SendEmail(
|
await _mailer.Received(1).SendEmail(
|
||||||
Arg.Is<Families2020RenewalMail>(email =>
|
Arg.Is<Families2019RenewalMail>(email =>
|
||||||
email.ToEmails.Contains("org@example.com") &&
|
email.ToEmails.Contains("org@example.com") &&
|
||||||
email.Subject == "Your Bitwarden Families renewal is updating" &&
|
email.Subject == "Your Bitwarden Families renewal is updating" &&
|
||||||
email.View.MonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US"))));
|
email.View.BaseMonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US")) &&
|
||||||
|
email.View.BaseAnnualRenewalPrice == familiesPlan.PasswordManager.BasePrice.ToString("C", new CultureInfo("en-US")) &&
|
||||||
|
email.View.DiscountAmount == $"{coupon.PercentOff}%"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -1635,8 +1652,11 @@ public class UpcomingInvoiceHandlerTests
|
|||||||
PlanType = PlanType.FamiliesAnnually2019
|
PlanType = PlanType.FamiliesAnnually2019
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var coupon = new Coupon { PercentOff = 25, Id = CouponIDs.Milestone3SubscriptionDiscount };
|
||||||
|
|
||||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||||
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
|
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
|
||||||
|
_stripeFacade.GetCoupon(CouponIDs.Milestone3SubscriptionDiscount).Returns(coupon);
|
||||||
_stripeEventUtilityService
|
_stripeEventUtilityService
|
||||||
.GetIdsFromMetadata(subscription.Metadata)
|
.GetIdsFromMetadata(subscription.Metadata)
|
||||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
|
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
|
||||||
@@ -1662,6 +1682,8 @@ public class UpcomingInvoiceHandlerTests
|
|||||||
o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&
|
o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&
|
||||||
o.ProrationBehavior == ProrationBehavior.None));
|
o.ProrationBehavior == ProrationBehavior.None));
|
||||||
|
|
||||||
|
await _stripeFacade.Received(1).GetCoupon(CouponIDs.Milestone3SubscriptionDiscount);
|
||||||
|
|
||||||
await _organizationRepository.Received(1).ReplaceAsync(
|
await _organizationRepository.Received(1).ReplaceAsync(
|
||||||
Arg.Is<Organization>(org =>
|
Arg.Is<Organization>(org =>
|
||||||
org.Id == _organizationId &&
|
org.Id == _organizationId &&
|
||||||
@@ -1671,10 +1693,13 @@ public class UpcomingInvoiceHandlerTests
|
|||||||
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
||||||
|
|
||||||
await _mailer.Received(1).SendEmail(
|
await _mailer.Received(1).SendEmail(
|
||||||
Arg.Is<Families2020RenewalMail>(email =>
|
Arg.Is<Families2019RenewalMail>(email =>
|
||||||
email.ToEmails.Contains("org@example.com") &&
|
email.ToEmails.Contains("org@example.com") &&
|
||||||
email.Subject == "Your Bitwarden Families renewal is updating" &&
|
email.Subject == "Your Bitwarden Families renewal is updating" &&
|
||||||
email.View.MonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US"))));
|
email.View.BaseMonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US")) &&
|
||||||
|
email.View.BaseAnnualRenewalPrice == familiesPlan.PasswordManager.BasePrice.ToString("C", new CultureInfo("en-US")) &&
|
||||||
|
email.View.DiscountAmount == $"{coupon.PercentOff}%"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -1748,8 +1773,11 @@ public class UpcomingInvoiceHandlerTests
|
|||||||
PlanType = PlanType.FamiliesAnnually2019
|
PlanType = PlanType.FamiliesAnnually2019
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var coupon = new Coupon { PercentOff = 25, Id = CouponIDs.Milestone3SubscriptionDiscount };
|
||||||
|
|
||||||
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
_stripeEventService.GetInvoice(parsedEvent).Returns(invoice);
|
||||||
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
|
_stripeFacade.GetCustomer(customerId, Arg.Any<CustomerGetOptions>()).Returns(customer);
|
||||||
|
_stripeFacade.GetCoupon(CouponIDs.Milestone3SubscriptionDiscount).Returns(coupon);
|
||||||
_stripeEventUtilityService
|
_stripeEventUtilityService
|
||||||
.GetIdsFromMetadata(subscription.Metadata)
|
.GetIdsFromMetadata(subscription.Metadata)
|
||||||
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
|
.Returns(new Tuple<Guid?, Guid?, Guid?>(_organizationId, null, null));
|
||||||
@@ -1777,6 +1805,8 @@ public class UpcomingInvoiceHandlerTests
|
|||||||
o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&
|
o.Discounts[0].Coupon == CouponIDs.Milestone3SubscriptionDiscount &&
|
||||||
o.ProrationBehavior == ProrationBehavior.None));
|
o.ProrationBehavior == ProrationBehavior.None));
|
||||||
|
|
||||||
|
await _stripeFacade.Received(1).GetCoupon(CouponIDs.Milestone3SubscriptionDiscount);
|
||||||
|
|
||||||
await _organizationRepository.Received(1).ReplaceAsync(
|
await _organizationRepository.Received(1).ReplaceAsync(
|
||||||
Arg.Is<Organization>(org =>
|
Arg.Is<Organization>(org =>
|
||||||
org.Id == _organizationId &&
|
org.Id == _organizationId &&
|
||||||
@@ -1786,10 +1816,13 @@ public class UpcomingInvoiceHandlerTests
|
|||||||
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
||||||
|
|
||||||
await _mailer.Received(1).SendEmail(
|
await _mailer.Received(1).SendEmail(
|
||||||
Arg.Is<Families2020RenewalMail>(email =>
|
Arg.Is<Families2019RenewalMail>(email =>
|
||||||
email.ToEmails.Contains("org@example.com") &&
|
email.ToEmails.Contains("org@example.com") &&
|
||||||
email.Subject == "Your Bitwarden Families renewal is updating" &&
|
email.Subject == "Your Bitwarden Families renewal is updating" &&
|
||||||
email.View.MonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US"))));
|
email.View.BaseMonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US")) &&
|
||||||
|
email.View.BaseAnnualRenewalPrice == familiesPlan.PasswordManager.BasePrice.ToString("C", new CultureInfo("en-US")) &&
|
||||||
|
email.View.DiscountAmount == $"{coupon.PercentOff}%"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -1879,6 +1912,12 @@ public class UpcomingInvoiceHandlerTests
|
|||||||
org.Plan == familiesPlan.Name &&
|
org.Plan == familiesPlan.Name &&
|
||||||
org.UsersGetPremium == familiesPlan.UsersGetPremium &&
|
org.UsersGetPremium == familiesPlan.UsersGetPremium &&
|
||||||
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
org.Seats == familiesPlan.PasswordManager.BaseSeats));
|
||||||
|
|
||||||
|
await _mailer.Received(1).SendEmail(
|
||||||
|
Arg.Is<Families2020RenewalMail>(email =>
|
||||||
|
email.ToEmails.Contains("org@example.com") &&
|
||||||
|
email.Subject == "Your Bitwarden Families renewal is updating" &&
|
||||||
|
email.View.MonthlyRenewalPrice == (familiesPlan.PasswordManager.BasePrice / 12).ToString("C", new CultureInfo("en-US"))));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -11,11 +11,11 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
<PackageReference Include="MartinCostello.Logging.XUnit" Version="0.5.1" />
|
<PackageReference Include="MartinCostello.Logging.XUnit" Version="0.7.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
<PackageReference Include="Rnwood.SmtpServer" Version="3.1.0-ci0868" />
|
<PackageReference Include="Rnwood.SmtpServer" Version="3.1.0-ci0868" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models;
|
||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
|
using Bit.Core.Platform.Push;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
@@ -95,7 +98,8 @@ public class SavePolicyCommandTests
|
|||||||
Substitute.For<IPolicyRepository>(),
|
Substitute.For<IPolicyRepository>(),
|
||||||
[new FakeSingleOrgPolicyValidator(), new FakeSingleOrgPolicyValidator()],
|
[new FakeSingleOrgPolicyValidator(), new FakeSingleOrgPolicyValidator()],
|
||||||
Substitute.For<TimeProvider>(),
|
Substitute.For<TimeProvider>(),
|
||||||
Substitute.For<IPostSavePolicySideEffect>()));
|
Substitute.For<IPostSavePolicySideEffect>(),
|
||||||
|
Substitute.For<IPushNotificationService>()));
|
||||||
Assert.Contains("Duplicate PolicyValidator for SingleOrg policy", exception.Message);
|
Assert.Contains("Duplicate PolicyValidator for SingleOrg policy", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,6 +364,103 @@ public class SavePolicyCommandTests
|
|||||||
.ExecuteSideEffectsAsync(default!, default!, default!);
|
.ExecuteSideEffectsAsync(default!, default!, default!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task VNextSaveAsync_SendsPushNotification(
|
||||||
|
[PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,
|
||||||
|
[Policy(PolicyType.SingleOrg, false)] Policy currentPolicy)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var fakePolicyValidator = new FakeSingleOrgPolicyValidator();
|
||||||
|
fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns("");
|
||||||
|
var sutProvider = SutProviderFactory([fakePolicyValidator]);
|
||||||
|
var savePolicyModel = new SavePolicyModel(policyUpdate);
|
||||||
|
|
||||||
|
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||||
|
sutProvider.GetDependency<IPolicyRepository>()
|
||||||
|
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)
|
||||||
|
.Returns(currentPolicy);
|
||||||
|
|
||||||
|
ArrangeOrganization(sutProvider, policyUpdate);
|
||||||
|
sutProvider.GetDependency<IPolicyRepository>()
|
||||||
|
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||||
|
.Returns([currentPolicy]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.VNextSaveAsync(savePolicyModel);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1)
|
||||||
|
.PushAsync(Arg.Is<PushNotification<SyncPolicyPushNotification>>(p =>
|
||||||
|
p.Type == PushType.PolicyChanged &&
|
||||||
|
p.Target == NotificationTarget.Organization &&
|
||||||
|
p.TargetId == policyUpdate.OrganizationId &&
|
||||||
|
p.ExcludeCurrentContext == false &&
|
||||||
|
p.Payload.OrganizationId == policyUpdate.OrganizationId &&
|
||||||
|
p.Payload.Policy.Id == result.Id &&
|
||||||
|
p.Payload.Policy.Type == policyUpdate.Type &&
|
||||||
|
p.Payload.Policy.Enabled == policyUpdate.Enabled &&
|
||||||
|
p.Payload.Policy.Data == policyUpdate.Data));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveAsync_SendsPushNotification([PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate)
|
||||||
|
{
|
||||||
|
var fakePolicyValidator = new FakeSingleOrgPolicyValidator();
|
||||||
|
fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns("");
|
||||||
|
var sutProvider = SutProviderFactory([fakePolicyValidator]);
|
||||||
|
|
||||||
|
ArrangeOrganization(sutProvider, policyUpdate);
|
||||||
|
sutProvider.GetDependency<IPolicyRepository>().GetManyByOrganizationIdAsync(policyUpdate.OrganizationId).Returns([]);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.SaveAsync(policyUpdate);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1)
|
||||||
|
.PushAsync(Arg.Is<PushNotification<SyncPolicyPushNotification>>(p =>
|
||||||
|
p.Type == PushType.PolicyChanged &&
|
||||||
|
p.Target == NotificationTarget.Organization &&
|
||||||
|
p.TargetId == policyUpdate.OrganizationId &&
|
||||||
|
p.ExcludeCurrentContext == false &&
|
||||||
|
p.Payload.OrganizationId == policyUpdate.OrganizationId &&
|
||||||
|
p.Payload.Policy.Id == result.Id &&
|
||||||
|
p.Payload.Policy.Type == policyUpdate.Type &&
|
||||||
|
p.Payload.Policy.Enabled == policyUpdate.Enabled &&
|
||||||
|
p.Payload.Policy.Data == policyUpdate.Data));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SaveAsync_ExistingPolicy_SendsPushNotificationWithUpdatedPolicy(
|
||||||
|
[PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate,
|
||||||
|
[Policy(PolicyType.SingleOrg, false)] Policy currentPolicy)
|
||||||
|
{
|
||||||
|
var fakePolicyValidator = new FakeSingleOrgPolicyValidator();
|
||||||
|
fakePolicyValidator.ValidateAsyncMock(policyUpdate, null).Returns("");
|
||||||
|
var sutProvider = SutProviderFactory([fakePolicyValidator]);
|
||||||
|
|
||||||
|
currentPolicy.OrganizationId = policyUpdate.OrganizationId;
|
||||||
|
sutProvider.GetDependency<IPolicyRepository>()
|
||||||
|
.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)
|
||||||
|
.Returns(currentPolicy);
|
||||||
|
|
||||||
|
ArrangeOrganization(sutProvider, policyUpdate);
|
||||||
|
sutProvider.GetDependency<IPolicyRepository>()
|
||||||
|
.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId)
|
||||||
|
.Returns([currentPolicy]);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.SaveAsync(policyUpdate);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1)
|
||||||
|
.PushAsync(Arg.Is<PushNotification<SyncPolicyPushNotification>>(p =>
|
||||||
|
p.Type == PushType.PolicyChanged &&
|
||||||
|
p.Target == NotificationTarget.Organization &&
|
||||||
|
p.TargetId == policyUpdate.OrganizationId &&
|
||||||
|
p.ExcludeCurrentContext == false &&
|
||||||
|
p.Payload.OrganizationId == policyUpdate.OrganizationId &&
|
||||||
|
p.Payload.Policy.Id == result.Id &&
|
||||||
|
p.Payload.Policy.Type == policyUpdate.Type &&
|
||||||
|
p.Payload.Policy.Enabled == policyUpdate.Enabled &&
|
||||||
|
p.Payload.Policy.Data == policyUpdate.Data));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a new SutProvider with the PolicyValidators registered in the Sut.
|
/// Returns a new SutProvider with the PolicyValidators registered in the Sut.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Auth.Sso;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Auth.UserFeatures.Sso;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class UserSsoOrganizationIdentifierQueryTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetSsoOrganizationIdentifierAsync_UserHasSingleConfirmedOrganization_ReturnsIdentifier(
|
||||||
|
SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,
|
||||||
|
Guid userId,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser.UserId = userId;
|
||||||
|
organizationUser.OrganizationId = organization.Id;
|
||||||
|
organizationUser.Status = OrganizationUserStatusType.Confirmed;
|
||||||
|
organization.Identifier = "test-org-identifier";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(userId)
|
||||||
|
.Returns([organizationUser]);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("test-org-identifier", result);
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyByUserAsync(userId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetByIdAsync(organization.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetSsoOrganizationIdentifierAsync_UserHasNoOrganizations_ReturnsNull(
|
||||||
|
SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,
|
||||||
|
Guid userId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(userId)
|
||||||
|
.Returns(Array.Empty<OrganizationUser>());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyByUserAsync(userId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.DidNotReceive()
|
||||||
|
.GetByIdAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetSsoOrganizationIdentifierAsync_UserHasMultipleConfirmedOrganizations_ReturnsNull(
|
||||||
|
SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUser organizationUser1,
|
||||||
|
OrganizationUser organizationUser2)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser1.UserId = userId;
|
||||||
|
organizationUser1.Status = OrganizationUserStatusType.Confirmed;
|
||||||
|
organizationUser2.UserId = userId;
|
||||||
|
organizationUser2.Status = OrganizationUserStatusType.Confirmed;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(userId)
|
||||||
|
.Returns([organizationUser1, organizationUser2]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyByUserAsync(userId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.DidNotReceive()
|
||||||
|
.GetByIdAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Invited)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Accepted)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Revoked)]
|
||||||
|
public async Task GetSsoOrganizationIdentifierAsync_UserHasOnlyInvitedOrganization_ReturnsNull(
|
||||||
|
OrganizationUserStatusType status,
|
||||||
|
SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser.UserId = userId;
|
||||||
|
organizationUser.Status = status;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(userId)
|
||||||
|
.Returns([organizationUser]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyByUserAsync(userId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.DidNotReceive()
|
||||||
|
.GetByIdAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetSsoOrganizationIdentifierAsync_UserHasMixedStatusOrganizations_OnlyOneConfirmed_ReturnsIdentifier(
|
||||||
|
SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,
|
||||||
|
Guid userId,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser confirmedOrgUser,
|
||||||
|
OrganizationUser invitedOrgUser,
|
||||||
|
OrganizationUser revokedOrgUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
confirmedOrgUser.UserId = userId;
|
||||||
|
confirmedOrgUser.OrganizationId = organization.Id;
|
||||||
|
confirmedOrgUser.Status = OrganizationUserStatusType.Confirmed;
|
||||||
|
|
||||||
|
invitedOrgUser.UserId = userId;
|
||||||
|
invitedOrgUser.Status = OrganizationUserStatusType.Invited;
|
||||||
|
|
||||||
|
revokedOrgUser.UserId = userId;
|
||||||
|
revokedOrgUser.Status = OrganizationUserStatusType.Revoked;
|
||||||
|
|
||||||
|
organization.Identifier = "mixed-status-org";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(userId)
|
||||||
|
.Returns(new[] { confirmedOrgUser, invitedOrgUser, revokedOrgUser });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("mixed-status-org", result);
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyByUserAsync(userId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetByIdAsync(organization.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetSsoOrganizationIdentifierAsync_OrganizationNotFound_ReturnsNull(
|
||||||
|
SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser.UserId = userId;
|
||||||
|
organizationUser.Status = OrganizationUserStatusType.Confirmed;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(userId)
|
||||||
|
.Returns([organizationUser]);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organizationUser.OrganizationId)
|
||||||
|
.Returns((Organization)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyByUserAsync(userId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetByIdAsync(organizationUser.OrganizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetSsoOrganizationIdentifierAsync_OrganizationIdentifierIsNull_ReturnsNull(
|
||||||
|
SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,
|
||||||
|
Guid userId,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser.UserId = userId;
|
||||||
|
organizationUser.OrganizationId = organization.Id;
|
||||||
|
organizationUser.Status = OrganizationUserStatusType.Confirmed;
|
||||||
|
organization.Identifier = null;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(userId)
|
||||||
|
.Returns(new[] { organizationUser });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyByUserAsync(userId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetByIdAsync(organization.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetSsoOrganizationIdentifierAsync_OrganizationIdentifierIsEmpty_ReturnsEmpty(
|
||||||
|
SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,
|
||||||
|
Guid userId,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser.UserId = userId;
|
||||||
|
organizationUser.OrganizationId = organization.Id;
|
||||||
|
organizationUser.Status = OrganizationUserStatusType.Confirmed;
|
||||||
|
organization.Identifier = string.Empty;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(userId)
|
||||||
|
.Returns(new[] { organizationUser });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(string.Empty, result);
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyByUserAsync(userId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetByIdAsync(organization.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -213,7 +213,8 @@ If you believe you need to change the version for a valid reason, please discuss
|
|||||||
LimitCollectionDeletion = true,
|
LimitCollectionDeletion = true,
|
||||||
AllowAdminAccessToAllCollectionItems = true,
|
AllowAdminAccessToAllCollectionItems = true,
|
||||||
UseOrganizationDomains = true,
|
UseOrganizationDomains = true,
|
||||||
UseAdminSponsoredFamilies = false
|
UseAdminSponsoredFamilies = false,
|
||||||
|
UsePhishingBlocker = false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ public class UpdateOrganizationLicenseCommandTests
|
|||||||
"Hash", "Signature", "SignatureBytes", "InstallationId", "Expires",
|
"Hash", "Signature", "SignatureBytes", "InstallationId", "Expires",
|
||||||
"ExpirationWithoutGracePeriod", "Token", "LimitCollectionCreationDeletion",
|
"ExpirationWithoutGracePeriod", "Token", "LimitCollectionCreationDeletion",
|
||||||
"LimitCollectionCreation", "LimitCollectionDeletion", "AllowAdminAccessToAllCollectionItems",
|
"LimitCollectionCreation", "LimitCollectionDeletion", "AllowAdminAccessToAllCollectionItems",
|
||||||
"UseOrganizationDomains", "UseAdminSponsoredFamilies", "UseAutomaticUserConfirmation") &&
|
"UseOrganizationDomains", "UseAdminSponsoredFamilies", "UseAutomaticUserConfirmation", "UsePhishingBlocker") &&
|
||||||
// Same property but different name, use explicit mapping
|
// Same property but different name, use explicit mapping
|
||||||
org.ExpirationDate == license.Expires));
|
org.ExpirationDate == license.Expires));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,30 +107,6 @@ public class CurrentContextTests
|
|||||||
Assert.Equal(deviceType, sutProvider.Sut.DeviceType);
|
Assert.Equal(deviceType, sutProvider.Sut.DeviceType);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task BuildAsync_HttpContext_SetsCloudflareFlags(
|
|
||||||
SutProvider<CurrentContext> sutProvider)
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var globalSettings = new Core.Settings.GlobalSettings();
|
|
||||||
sutProvider.Sut.BotScore = null;
|
|
||||||
// Arrange
|
|
||||||
var botScore = 85;
|
|
||||||
httpContext.Request.Headers["X-Cf-Bot-Score"] = botScore.ToString();
|
|
||||||
httpContext.Request.Headers["X-Cf-Worked-Proxied"] = "1";
|
|
||||||
httpContext.Request.Headers["X-Cf-Is-Bot"] = "1";
|
|
||||||
httpContext.Request.Headers["X-Cf-Maybe-Bot"] = "1";
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await sutProvider.Sut.BuildAsync(httpContext, globalSettings);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(sutProvider.Sut.CloudflareWorkerProxied);
|
|
||||||
Assert.True(sutProvider.Sut.IsBot);
|
|
||||||
Assert.True(sutProvider.Sut.MaybeBot);
|
|
||||||
Assert.Equal(botScore, sutProvider.Sut.BotScore);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task BuildAsync_HttpContext_SetsClientVersion(
|
public async Task BuildAsync_HttpContext_SetsClientVersion(
|
||||||
SutProvider<CurrentContext> sutProvider)
|
SutProvider<CurrentContext> sutProvider)
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ public class SendGridMailDeliveryServiceTests : IDisposable
|
|||||||
Assert.Equal(mailMessage.HtmlContent, msg.HtmlContent);
|
Assert.Equal(mailMessage.HtmlContent, msg.HtmlContent);
|
||||||
Assert.Equal(mailMessage.TextContent, msg.PlainTextContent);
|
Assert.Equal(mailMessage.TextContent, msg.PlainTextContent);
|
||||||
|
|
||||||
Assert.Contains("type:Cateogry", msg.Categories);
|
Assert.Contains("type:Category", msg.Categories);
|
||||||
Assert.Contains(msg.Categories, x => x.StartsWith("env:"));
|
Assert.Contains(msg.Categories, x => x.StartsWith("env:"));
|
||||||
Assert.Contains(msg.Categories, x => x.StartsWith("sender:"));
|
Assert.Contains(msg.Categories, x => x.StartsWith("sender:"));
|
||||||
|
|
||||||
|
|||||||
@@ -44,14 +44,17 @@ internal class CustomValidatorRequestContextCustomization : ICustomization
|
|||||||
/// <see cref="CustomValidatorRequestContext.TwoFactorRecoveryRequested"/>, and
|
/// <see cref="CustomValidatorRequestContext.TwoFactorRecoveryRequested"/>, and
|
||||||
/// <see cref="CustomValidatorRequestContext.SsoRequired" /> should initialize false,
|
/// <see cref="CustomValidatorRequestContext.SsoRequired" /> should initialize false,
|
||||||
/// and are made truthy in context upon evaluation of a request. Do not allow AutoFixture to eagerly make these
|
/// and are made truthy in context upon evaluation of a request. Do not allow AutoFixture to eagerly make these
|
||||||
/// truthy; that is the responsibility of the <see cref="Bit.Identity.IdentityServer.RequestValidators.BaseRequestValidator{T}" />
|
/// truthy; that is the responsibility of the <see cref="Bit.Identity.IdentityServer.RequestValidators.BaseRequestValidator{T}" />.
|
||||||
|
/// ValidationErrorResult and CustomResponse should also be null initially; they are hydrated during the validation process.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Customize(IFixture fixture)
|
public void Customize(IFixture fixture)
|
||||||
{
|
{
|
||||||
fixture.Customize<CustomValidatorRequestContext>(composer => composer
|
fixture.Customize<CustomValidatorRequestContext>(composer => composer
|
||||||
.With(o => o.RememberMeRequested, false)
|
.With(o => o.RememberMeRequested, false)
|
||||||
.With(o => o.TwoFactorRecoveryRequested, false)
|
.With(o => o.TwoFactorRecoveryRequested, false)
|
||||||
.With(o => o.SsoRequired, false));
|
.With(o => o.SsoRequired, false)
|
||||||
|
.With(o => o.ValidationErrorResult, () => null)
|
||||||
|
.With(o => o.CustomResponse, () => null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ using Bit.Identity.IdentityServer;
|
|||||||
using Bit.Identity.IdentityServer.RequestValidators;
|
using Bit.Identity.IdentityServer.RequestValidators;
|
||||||
using Bit.Identity.Test.Wrappers;
|
using Bit.Identity.Test.Wrappers;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Duende.IdentityModel;
|
||||||
using Duende.IdentityServer.Validation;
|
using Duende.IdentityServer.Validation;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -42,6 +43,7 @@ public class BaseRequestValidatorTests
|
|||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IDeviceValidator _deviceValidator;
|
private readonly IDeviceValidator _deviceValidator;
|
||||||
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
||||||
|
private readonly ISsoRequestValidator _ssoRequestValidator;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly FakeLogger<BaseRequestValidatorTests> _logger;
|
private readonly FakeLogger<BaseRequestValidatorTests> _logger;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
@@ -66,6 +68,7 @@ public class BaseRequestValidatorTests
|
|||||||
_eventService = Substitute.For<IEventService>();
|
_eventService = Substitute.For<IEventService>();
|
||||||
_deviceValidator = Substitute.For<IDeviceValidator>();
|
_deviceValidator = Substitute.For<IDeviceValidator>();
|
||||||
_twoFactorAuthenticationValidator = Substitute.For<ITwoFactorAuthenticationValidator>();
|
_twoFactorAuthenticationValidator = Substitute.For<ITwoFactorAuthenticationValidator>();
|
||||||
|
_ssoRequestValidator = Substitute.For<ISsoRequestValidator>();
|
||||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||||
_logger = new FakeLogger<BaseRequestValidatorTests>();
|
_logger = new FakeLogger<BaseRequestValidatorTests>();
|
||||||
_currentContext = Substitute.For<ICurrentContext>();
|
_currentContext = Substitute.For<ICurrentContext>();
|
||||||
@@ -87,6 +90,7 @@ public class BaseRequestValidatorTests
|
|||||||
_eventService,
|
_eventService,
|
||||||
_deviceValidator,
|
_deviceValidator,
|
||||||
_twoFactorAuthenticationValidator,
|
_twoFactorAuthenticationValidator,
|
||||||
|
_ssoRequestValidator,
|
||||||
_organizationUserRepository,
|
_organizationUserRepository,
|
||||||
_logger,
|
_logger,
|
||||||
_currentContext,
|
_currentContext,
|
||||||
@@ -159,6 +163,7 @@ public class BaseRequestValidatorTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
|
||||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
|
||||||
// 1 -> to pass
|
// 1 -> to pass
|
||||||
_sut.isValid = true;
|
_sut.isValid = true;
|
||||||
|
|
||||||
@@ -170,9 +175,9 @@ public class BaseRequestValidatorTests
|
|||||||
|
|
||||||
// 4 -> set up device validator to fail
|
// 4 -> set up device validator to fail
|
||||||
requestContext.KnownDevice = false;
|
requestContext.KnownDevice = false;
|
||||||
tokenRequest.GrantType = "password";
|
tokenRequest.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
_deviceValidator
|
_deviceValidator
|
||||||
.ValidateRequestDeviceAsync(Arg.Any<ValidatedTokenRequest>(), Arg.Any<CustomValidatorRequestContext>())
|
.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||||
.Returns(Task.FromResult(false));
|
.Returns(Task.FromResult(false));
|
||||||
|
|
||||||
// 5 -> not legacy user
|
// 5 -> not legacy user
|
||||||
@@ -200,6 +205,7 @@ public class BaseRequestValidatorTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
|
||||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
|
||||||
// 1 -> to pass
|
// 1 -> to pass
|
||||||
_sut.isValid = true;
|
_sut.isValid = true;
|
||||||
|
|
||||||
@@ -211,12 +217,13 @@ public class BaseRequestValidatorTests
|
|||||||
|
|
||||||
// 4 -> set up device validator to pass
|
// 4 -> set up device validator to pass
|
||||||
_deviceValidator
|
_deviceValidator
|
||||||
.ValidateRequestDeviceAsync(Arg.Any<ValidatedTokenRequest>(), Arg.Any<CustomValidatorRequestContext>())
|
.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||||
.Returns(Task.FromResult(true));
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
// 5 -> not legacy user
|
// 5 -> not legacy user
|
||||||
_userService.IsLegacyUser(Arg.Any<string>())
|
_userService.IsLegacyUser(Arg.Any<string>())
|
||||||
.Returns(false);
|
.Returns(false);
|
||||||
|
|
||||||
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||||
{
|
{
|
||||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||||
@@ -244,6 +251,7 @@ public class BaseRequestValidatorTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
|
||||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
|
||||||
// 1 -> to pass
|
// 1 -> to pass
|
||||||
_sut.isValid = true;
|
_sut.isValid = true;
|
||||||
|
|
||||||
@@ -270,12 +278,13 @@ public class BaseRequestValidatorTests
|
|||||||
|
|
||||||
// 4 -> set up device validator to pass
|
// 4 -> set up device validator to pass
|
||||||
_deviceValidator
|
_deviceValidator
|
||||||
.ValidateRequestDeviceAsync(Arg.Any<ValidatedTokenRequest>(), Arg.Any<CustomValidatorRequestContext>())
|
.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||||
.Returns(Task.FromResult(true));
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
// 5 -> not legacy user
|
// 5 -> not legacy user
|
||||||
_userService.IsLegacyUser(Arg.Any<string>())
|
_userService.IsLegacyUser(Arg.Any<string>())
|
||||||
.Returns(false);
|
.Returns(false);
|
||||||
|
|
||||||
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||||
{
|
{
|
||||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||||
@@ -307,6 +316,7 @@ public class BaseRequestValidatorTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
|
||||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
|
||||||
// 1 -> to pass
|
// 1 -> to pass
|
||||||
_sut.isValid = true;
|
_sut.isValid = true;
|
||||||
|
|
||||||
@@ -327,10 +337,19 @@ public class BaseRequestValidatorTests
|
|||||||
|
|
||||||
// 2 -> will result to false with no extra configuration
|
// 2 -> will result to false with no extra configuration
|
||||||
// 3 -> set two factor to be required
|
// 3 -> set two factor to be required
|
||||||
|
requestContext.User.TwoFactorProviders = "{\"1\":{\"Enabled\":true,\"MetaData\":{\"Email\":\"user@test.dev\"}}}";
|
||||||
_twoFactorAuthenticationValidator
|
_twoFactorAuthenticationValidator
|
||||||
.RequiresTwoFactorAsync(Arg.Any<User>(), tokenRequest)
|
.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
|
||||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(true, null)));
|
.Returns(Task.FromResult(new Tuple<bool, Organization>(true, null)));
|
||||||
|
|
||||||
|
_twoFactorAuthenticationValidator
|
||||||
|
.BuildTwoFactorResultAsync(requestContext.User, null)
|
||||||
|
.Returns(Task.FromResult(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "TwoFactorProviders", new[] { "0", "1" } },
|
||||||
|
{ "TwoFactorProviders2", new Dictionary<string, object>{{"Email", null}} }
|
||||||
|
}));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await _sut.ValidateAsync(context);
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
@@ -338,7 +357,10 @@ public class BaseRequestValidatorTests
|
|||||||
Assert.True(context.GrantResult.IsError);
|
Assert.True(context.GrantResult.IsError);
|
||||||
|
|
||||||
// Assert that the auth request was NOT consumed
|
// Assert that the auth request was NOT consumed
|
||||||
await _authRequestRepository.DidNotReceive().ReplaceAsync(Arg.Any<AuthRequest>());
|
await _authRequestRepository.DidNotReceive().ReplaceAsync(authRequest);
|
||||||
|
|
||||||
|
// Assert that the error is for 2fa
|
||||||
|
Assert.Equal("Two-factor authentication required.", context.GrantResult.ErrorDescription);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -428,6 +450,7 @@ public class BaseRequestValidatorTests
|
|||||||
{ "TwoFactorProviders", new[] { "0", "1" } },
|
{ "TwoFactorProviders", new[] { "0", "1" } },
|
||||||
{ "TwoFactorProviders2", new Dictionary<string, object>() }
|
{ "TwoFactorProviders2", new Dictionary<string, object>() }
|
||||||
};
|
};
|
||||||
|
|
||||||
_twoFactorAuthenticationValidator
|
_twoFactorAuthenticationValidator
|
||||||
.BuildTwoFactorResultAsync(user, null)
|
.BuildTwoFactorResultAsync(user, null)
|
||||||
.Returns(Task.FromResult(twoFactorResultDict));
|
.Returns(Task.FromResult(twoFactorResultDict));
|
||||||
@@ -436,6 +459,8 @@ public class BaseRequestValidatorTests
|
|||||||
await _sut.ValidateAsync(context);
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
Assert.Equal("Two-factor authentication required.", context.GrantResult.ErrorDescription);
|
||||||
|
|
||||||
// Verify that the failed 2FA email was NOT sent for remember token expiration
|
// Verify that the failed 2FA email was NOT sent for remember token expiration
|
||||||
await _mailService.DidNotReceive()
|
await _mailService.DidNotReceive()
|
||||||
.SendFailedTwoFactorAttemptEmailAsync(Arg.Any<string>(), Arg.Any<TwoFactorProviderType>(),
|
.SendFailedTwoFactorAttemptEmailAsync(Arg.Any<string>(), Arg.Any<TwoFactorProviderType>(),
|
||||||
@@ -1286,6 +1311,343 @@ public class BaseRequestValidatorTests
|
|||||||
.ValidateAsync(requestContext.User, requestContext);
|
.ValidateAsync(requestContext.User, requestContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that when RedirectOnSsoRequired is DISABLED, the legacy SSO validation path is used.
|
||||||
|
/// This validates the deprecated RequireSsoLoginAsync method is called and SSO requirement
|
||||||
|
/// is checked using the old PolicyService.AnyPoliciesApplicableToUserAsync approach.
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(true)]
|
||||||
|
[BitAutoData(false)]
|
||||||
|
public async Task ValidateAsync_RedirectOnSsoRequired_Disabled_UsesLegacySsoValidation(
|
||||||
|
bool recoveryCodeFeatureEnabled,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
|
||||||
|
GrantValidationResult grantResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(recoveryCodeFeatureEnabled);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(false);
|
||||||
|
|
||||||
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
_sut.isValid = true;
|
||||||
|
|
||||||
|
tokenRequest.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
|
||||||
|
// SSO is required via legacy path
|
||||||
|
_policyService.AnyPoliciesApplicableToUserAsync(
|
||||||
|
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(context.GrantResult.IsError);
|
||||||
|
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
|
||||||
|
Assert.Equal("SSO authentication is required.", errorResponse.Message);
|
||||||
|
|
||||||
|
// Verify legacy path was used
|
||||||
|
await _policyService.Received(1).AnyPoliciesApplicableToUserAsync(
|
||||||
|
requestContext.User.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
||||||
|
|
||||||
|
// Verify new SsoRequestValidator was NOT called
|
||||||
|
await _ssoRequestValidator.DidNotReceive().ValidateAsync(
|
||||||
|
Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>(), Arg.Any<CustomValidatorRequestContext>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that when RedirectOnSsoRequired is ENABLED, the new ISsoRequestValidator is used
|
||||||
|
/// instead of the legacy RequireSsoLoginAsync method.
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(true)]
|
||||||
|
[BitAutoData(false)]
|
||||||
|
public async Task ValidateAsync_RedirectOnSsoRequired_Enabled_UsesNewSsoRequestValidator(
|
||||||
|
bool recoveryCodeFeatureEnabled,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
|
||||||
|
GrantValidationResult grantResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(recoveryCodeFeatureEnabled);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(true);
|
||||||
|
|
||||||
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
_sut.isValid = true;
|
||||||
|
|
||||||
|
tokenRequest.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
|
||||||
|
// Configure SsoRequestValidator to indicate SSO is required
|
||||||
|
_ssoRequestValidator.ValidateAsync(
|
||||||
|
Arg.Any<User>(),
|
||||||
|
Arg.Any<ValidatedTokenRequest>(),
|
||||||
|
Arg.Any<CustomValidatorRequestContext>())
|
||||||
|
.Returns(Task.FromResult(false)); // false = SSO required
|
||||||
|
|
||||||
|
// Set up the ValidationErrorResult that SsoRequestValidator would set
|
||||||
|
requestContext.ValidationErrorResult = new ValidationResult
|
||||||
|
{
|
||||||
|
IsError = true,
|
||||||
|
Error = "sso_required",
|
||||||
|
ErrorDescription = "SSO authentication is required."
|
||||||
|
};
|
||||||
|
requestContext.CustomResponse = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(context.GrantResult.IsError);
|
||||||
|
|
||||||
|
// Verify new SsoRequestValidator was called
|
||||||
|
await _ssoRequestValidator.Received(1).ValidateAsync(
|
||||||
|
requestContext.User,
|
||||||
|
tokenRequest,
|
||||||
|
requestContext);
|
||||||
|
|
||||||
|
// Verify legacy path was NOT used
|
||||||
|
await _policyService.DidNotReceive().AnyPoliciesApplicableToUserAsync(
|
||||||
|
Arg.Any<Guid>(), Arg.Any<PolicyType>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that when RedirectOnSsoRequired is ENABLED and SSO is NOT required,
|
||||||
|
/// authentication continues successfully through the new validation path.
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(true)]
|
||||||
|
[BitAutoData(false)]
|
||||||
|
public async Task ValidateAsync_RedirectOnSsoRequired_Enabled_SsoNotRequired_SuccessfulLogin(
|
||||||
|
bool recoveryCodeFeatureEnabled,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
|
||||||
|
GrantValidationResult grantResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(recoveryCodeFeatureEnabled);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(true);
|
||||||
|
|
||||||
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
_sut.isValid = true;
|
||||||
|
|
||||||
|
tokenRequest.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
tokenRequest.ClientId = "web";
|
||||||
|
|
||||||
|
// SsoRequestValidator returns true (SSO not required)
|
||||||
|
_ssoRequestValidator.ValidateAsync(
|
||||||
|
Arg.Any<User>(),
|
||||||
|
Arg.Any<ValidatedTokenRequest>(),
|
||||||
|
Arg.Any<CustomValidatorRequestContext>())
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
|
// No 2FA required
|
||||||
|
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
|
||||||
|
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||||
|
|
||||||
|
// Device validation passes
|
||||||
|
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
|
// User is not legacy
|
||||||
|
_userService.IsLegacyUser(Arg.Any<string>()).Returns(false);
|
||||||
|
|
||||||
|
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||||
|
{
|
||||||
|
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||||
|
"test-private-key",
|
||||||
|
"test-public-key"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(context.GrantResult.IsError);
|
||||||
|
await _eventService.Received(1).LogUserEventAsync(requestContext.User.Id, EventType.User_LoggedIn);
|
||||||
|
|
||||||
|
// Verify new validator was used
|
||||||
|
await _ssoRequestValidator.Received(1).ValidateAsync(
|
||||||
|
requestContext.User,
|
||||||
|
tokenRequest,
|
||||||
|
requestContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that when RedirectOnSsoRequired is ENABLED and SSO validation returns a custom response
|
||||||
|
/// (e.g., with organization identifier), that custom response is properly propagated to the result.
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(true)]
|
||||||
|
[BitAutoData(false)]
|
||||||
|
public async Task ValidateAsync_RedirectOnSsoRequired_Enabled_PropagatesCustomResponse(
|
||||||
|
bool recoveryCodeFeatureEnabled,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
|
||||||
|
GrantValidationResult grantResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(recoveryCodeFeatureEnabled);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(true);
|
||||||
|
_sut.isValid = true;
|
||||||
|
|
||||||
|
tokenRequest.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
|
||||||
|
// SsoRequestValidator sets custom response with organization identifier
|
||||||
|
requestContext.ValidationErrorResult = new ValidationResult
|
||||||
|
{
|
||||||
|
IsError = true,
|
||||||
|
Error = "sso_required",
|
||||||
|
ErrorDescription = "SSO authentication is required."
|
||||||
|
};
|
||||||
|
requestContext.CustomResponse = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") },
|
||||||
|
{ "SsoOrganizationIdentifier", "test-org-identifier" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
|
||||||
|
_ssoRequestValidator.ValidateAsync(
|
||||||
|
Arg.Any<User>(),
|
||||||
|
Arg.Any<ValidatedTokenRequest>(),
|
||||||
|
Arg.Any<CustomValidatorRequestContext>())
|
||||||
|
.Returns(Task.FromResult(false));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(context.GrantResult.IsError);
|
||||||
|
Assert.NotNull(context.GrantResult.CustomResponse);
|
||||||
|
Assert.Contains("SsoOrganizationIdentifier", context.CustomValidatorRequestContext.CustomResponse);
|
||||||
|
Assert.Equal("test-org-identifier", context.CustomValidatorRequestContext.CustomResponse["SsoOrganizationIdentifier"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that when RedirectOnSsoRequired is DISABLED and a user with 2FA recovery completes recovery,
|
||||||
|
/// but SSO is required, the legacy error message is returned (without the recovery-specific message).
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task ValidateAsync_RedirectOnSsoRequired_Disabled_RecoveryWithSso_LegacyMessage(
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
|
||||||
|
GrantValidationResult grantResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(true);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(false);
|
||||||
|
|
||||||
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
_sut.isValid = true;
|
||||||
|
|
||||||
|
// Recovery code scenario
|
||||||
|
tokenRequest.Raw["TwoFactorProvider"] = ((int)TwoFactorProviderType.RecoveryCode).ToString();
|
||||||
|
tokenRequest.Raw["TwoFactorToken"] = "valid-recovery-code";
|
||||||
|
|
||||||
|
// 2FA with recovery
|
||||||
|
_twoFactorAuthenticationValidator
|
||||||
|
.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
|
||||||
|
.Returns(Task.FromResult(new Tuple<bool, Organization>(true, null)));
|
||||||
|
|
||||||
|
_twoFactorAuthenticationValidator
|
||||||
|
.VerifyTwoFactorAsync(requestContext.User, null, TwoFactorProviderType.RecoveryCode, "valid-recovery-code")
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
|
// SSO is required (legacy check)
|
||||||
|
_policyService.AnyPoliciesApplicableToUserAsync(
|
||||||
|
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(context.GrantResult.IsError);
|
||||||
|
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
|
||||||
|
|
||||||
|
// Legacy behavior: recovery-specific message IS shown even without RedirectOnSsoRequired
|
||||||
|
Assert.Equal("Two-factor recovery has been performed. SSO authentication is required.", errorResponse.Message);
|
||||||
|
|
||||||
|
// But legacy validation path was used
|
||||||
|
await _policyService.Received(1).AnyPoliciesApplicableToUserAsync(
|
||||||
|
requestContext.User.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that when RedirectOnSsoRequired is ENABLED and recovery code is used for SSO-required user,
|
||||||
|
/// the SsoRequestValidator provides the recovery-specific error message.
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task ValidateAsync_RedirectOnSsoRequired_Enabled_RecoveryWithSso_NewValidatorMessage(
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
|
||||||
|
GrantValidationResult grantResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(true);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(true);
|
||||||
|
|
||||||
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
_sut.isValid = true;
|
||||||
|
|
||||||
|
// Recovery code scenario
|
||||||
|
tokenRequest.Raw["TwoFactorProvider"] = ((int)TwoFactorProviderType.RecoveryCode).ToString();
|
||||||
|
tokenRequest.Raw["TwoFactorToken"] = "valid-recovery-code";
|
||||||
|
|
||||||
|
// 2FA with recovery
|
||||||
|
_twoFactorAuthenticationValidator
|
||||||
|
.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
|
||||||
|
.Returns(Task.FromResult(new Tuple<bool, Organization>(true, null)));
|
||||||
|
|
||||||
|
_twoFactorAuthenticationValidator
|
||||||
|
.VerifyTwoFactorAsync(requestContext.User, null, TwoFactorProviderType.RecoveryCode, "valid-recovery-code")
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
|
// SsoRequestValidator handles the recovery + SSO scenario
|
||||||
|
requestContext.TwoFactorRecoveryRequested = true;
|
||||||
|
requestContext.ValidationErrorResult = new ValidationResult
|
||||||
|
{
|
||||||
|
IsError = true,
|
||||||
|
Error = "sso_required",
|
||||||
|
ErrorDescription = "Two-factor recovery has been performed. SSO authentication is required."
|
||||||
|
};
|
||||||
|
requestContext.CustomResponse = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "ErrorModel", new ErrorResponseModel("Two-factor recovery has been performed. SSO authentication is required.") }
|
||||||
|
};
|
||||||
|
|
||||||
|
_ssoRequestValidator.ValidateAsync(
|
||||||
|
Arg.Any<User>(),
|
||||||
|
Arg.Any<ValidatedTokenRequest>(),
|
||||||
|
Arg.Any<CustomValidatorRequestContext>())
|
||||||
|
.Returns(Task.FromResult(false));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(context.GrantResult.IsError);
|
||||||
|
var errorResponse = (ErrorResponseModel)context.CustomValidatorRequestContext.CustomResponse["ErrorModel"];
|
||||||
|
Assert.Equal("Two-factor recovery has been performed. SSO authentication is required.", errorResponse.Message);
|
||||||
|
|
||||||
|
// Verify new validator was used
|
||||||
|
await _ssoRequestValidator.Received(1).ValidateAsync(
|
||||||
|
requestContext.User,
|
||||||
|
tokenRequest,
|
||||||
|
Arg.Is<CustomValidatorRequestContext>(ctx => ctx.TwoFactorRecoveryRequested));
|
||||||
|
|
||||||
|
// Verify legacy path was NOT used
|
||||||
|
await _policyService.DidNotReceive().AnyPoliciesApplicableToUserAsync(
|
||||||
|
Arg.Any<Guid>(), Arg.Any<PolicyType>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
}
|
||||||
|
|
||||||
private BaseRequestValidationContextFake CreateContext(
|
private BaseRequestValidationContextFake CreateContext(
|
||||||
ValidatedTokenRequest tokenRequest,
|
ValidatedTokenRequest tokenRequest,
|
||||||
CustomValidatorRequestContext requestContext,
|
CustomValidatorRequestContext requestContext,
|
||||||
|
|||||||
469
test/Identity.Test/IdentityServer/SsoRequestValidatorTests.cs
Normal file
469
test/Identity.Test/IdentityServer/SsoRequestValidatorTests.cs
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Auth.Sso;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Identity.IdentityServer;
|
||||||
|
using Bit.Identity.IdentityServer.Enums;
|
||||||
|
using Bit.Identity.IdentityServer.RequestValidationConstants;
|
||||||
|
using Bit.Identity.IdentityServer.RequestValidators;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Duende.IdentityModel;
|
||||||
|
using Duende.IdentityServer.Validation;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using AuthFixtures = Bit.Identity.Test.AutoFixture;
|
||||||
|
|
||||||
|
namespace Bit.Identity.Test.IdentityServer;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class SsoRequestValidatorTests
|
||||||
|
{
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(OidcConstants.GrantTypes.AuthorizationCode)]
|
||||||
|
[BitAutoData(OidcConstants.GrantTypes.ClientCredentials)]
|
||||||
|
public async void ValidateAsync_GrantTypeIgnoresSsoRequirement_ReturnsTrue(
|
||||||
|
string grantType,
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = grantType;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.False(context.SsoRequired);
|
||||||
|
Assert.Null(context.ValidationErrorResult);
|
||||||
|
Assert.Null(context.CustomResponse);
|
||||||
|
|
||||||
|
// Should not check policies since grant type allows bypass
|
||||||
|
await sutProvider.GetDependency<IPolicyService>().DidNotReceive()
|
||||||
|
.AnyPoliciesApplicableToUserAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await sutProvider.GetDependency<IPolicyRequirementQuery>().DidNotReceive()
|
||||||
|
.GetAsync<RequireSsoPolicyRequirement>(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoNotRequired_RequirementPolicyFeatureFlagEnabled_ReturnsTrue(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = false };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.False(context.SsoRequired);
|
||||||
|
Assert.Null(context.ValidationErrorResult);
|
||||||
|
Assert.Null(context.CustomResponse);
|
||||||
|
|
||||||
|
// Should use the new policy requirement query when feature flag is enabled
|
||||||
|
await sutProvider.GetDependency<IPolicyRequirementQuery>().Received(1).GetAsync<RequireSsoPolicyRequirement>(user.Id);
|
||||||
|
await sutProvider.GetDependency<IPolicyService>().DidNotReceive()
|
||||||
|
.AnyPoliciesApplicableToUserAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoNotRequired_RequirementPolicyFeatureFlagDisabled_ReturnsTrue(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(false);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(
|
||||||
|
user.Id,
|
||||||
|
PolicyType.RequireSso,
|
||||||
|
OrganizationUserStatusType.Confirmed)
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.False(context.SsoRequired);
|
||||||
|
Assert.Null(context.ValidationErrorResult);
|
||||||
|
Assert.Null(context.CustomResponse);
|
||||||
|
|
||||||
|
// Should use the legacy policy service when feature flag is disabled
|
||||||
|
await sutProvider.GetDependency<IPolicyService>().Received(1).AnyPoliciesApplicableToUserAsync(
|
||||||
|
user.Id,
|
||||||
|
PolicyType.RequireSso,
|
||||||
|
OrganizationUserStatusType.Confirmed);
|
||||||
|
await sutProvider.GetDependency<IPolicyRequirementQuery>().DidNotReceive()
|
||||||
|
.GetAsync<RequireSsoPolicyRequirement>(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoRequired_RequirementPolicyFeatureFlagEnabled_ReturnsFalse(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = true };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id)
|
||||||
|
.Returns((string)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.True(context.SsoRequired);
|
||||||
|
Assert.NotNull(context.ValidationErrorResult);
|
||||||
|
Assert.True(context.ValidationErrorResult.IsError);
|
||||||
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.ValidationErrorResult.Error);
|
||||||
|
Assert.Equal(SsoConstants.RequestErrors.SsoRequiredDescription, context.ValidationErrorResult.ErrorDescription);
|
||||||
|
|
||||||
|
Assert.NotNull(context.CustomResponse);
|
||||||
|
Assert.True(context.CustomResponse.ContainsKey(CustomResponseConstants.ResponseKeys.ErrorModel));
|
||||||
|
Assert.False(context.CustomResponse.ContainsKey(CustomResponseConstants.ResponseKeys.SsoOrganizationIdentifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoRequired_RequirementPolicyFeatureFlagDisabled_ReturnsFalse(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(false);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(
|
||||||
|
user.Id,
|
||||||
|
PolicyType.RequireSso,
|
||||||
|
OrganizationUserStatusType.Confirmed)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id)
|
||||||
|
.Returns((string)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.True(context.SsoRequired);
|
||||||
|
Assert.NotNull(context.ValidationErrorResult);
|
||||||
|
Assert.True(context.ValidationErrorResult.IsError);
|
||||||
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.ValidationErrorResult.Error);
|
||||||
|
Assert.Equal(SsoConstants.RequestErrors.SsoRequiredDescription, context.ValidationErrorResult.ErrorDescription);
|
||||||
|
|
||||||
|
Assert.NotNull(context.CustomResponse);
|
||||||
|
Assert.True(context.CustomResponse.ContainsKey("ErrorModel"));
|
||||||
|
Assert.False(context.CustomResponse.ContainsKey("SsoOrganizationIdentifier"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoRequired_TwoFactorRecoveryRequested_ReturnsFalse_WithSpecialMessage(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
context.TwoFactorRecoveryRequested = true;
|
||||||
|
context.TwoFactorRequired = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = true };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id)
|
||||||
|
.Returns((string)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.True(context.SsoRequired);
|
||||||
|
Assert.NotNull(context.ValidationErrorResult);
|
||||||
|
Assert.True(context.ValidationErrorResult.IsError);
|
||||||
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.ValidationErrorResult.Error);
|
||||||
|
Assert.Equal("Two-factor recovery has been performed. SSO authentication is required.",
|
||||||
|
context.ValidationErrorResult.ErrorDescription);
|
||||||
|
|
||||||
|
Assert.NotNull(context.CustomResponse);
|
||||||
|
Assert.True(context.CustomResponse.ContainsKey("ErrorModel"));
|
||||||
|
Assert.False(context.CustomResponse.ContainsKey("SsoOrganizationIdentifier"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoRequired_TwoFactorRequiredButNotRecovery_ReturnsFalse_WithStandardMessage(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = true };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id)
|
||||||
|
.Returns((string)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.True(context.SsoRequired);
|
||||||
|
Assert.NotNull(context.ValidationErrorResult);
|
||||||
|
Assert.True(context.ValidationErrorResult.IsError);
|
||||||
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.ValidationErrorResult.Error);
|
||||||
|
Assert.Equal(SsoConstants.RequestErrors.SsoRequiredDescription, context.ValidationErrorResult.ErrorDescription);
|
||||||
|
|
||||||
|
Assert.NotNull(context.CustomResponse);
|
||||||
|
Assert.True(context.CustomResponse.ContainsKey("ErrorModel"));
|
||||||
|
Assert.False(context.CustomResponse.ContainsKey("SsoOrganizationIdentifier"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(OidcConstants.GrantTypes.Password)]
|
||||||
|
[BitAutoData(OidcConstants.GrantTypes.RefreshToken)]
|
||||||
|
[BitAutoData(CustomGrantTypes.WebAuthn)]
|
||||||
|
public async void ValidateAsync_VariousGrantTypes_SsoRequired_ReturnsFalse(
|
||||||
|
string grantType,
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = grantType;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = true };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id)
|
||||||
|
.Returns((string)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.True(context.SsoRequired);
|
||||||
|
Assert.NotNull(context.ValidationErrorResult);
|
||||||
|
Assert.True(context.ValidationErrorResult.IsError);
|
||||||
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.ValidationErrorResult.Error);
|
||||||
|
Assert.Equal(SsoConstants.RequestErrors.SsoRequiredDescription, context.ValidationErrorResult.ErrorDescription);
|
||||||
|
Assert.NotNull(context.CustomResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_ContextSsoRequiredUpdated_RegardlessOfInitialValue(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
context.SsoRequired = true; // Start with true to ensure it gets updated
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = false };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.False(context.SsoRequired); // Should be updated to false
|
||||||
|
Assert.Null(context.ValidationErrorResult);
|
||||||
|
Assert.Null(context.CustomResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoRequired_WithOrganizationIdentifier_IncludesIdentifierInResponse(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string orgIdentifier = "test-organization";
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
context.User = user;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = true };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id)
|
||||||
|
.Returns(orgIdentifier);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.True(context.SsoRequired);
|
||||||
|
Assert.NotNull(context.CustomResponse);
|
||||||
|
Assert.True(context.CustomResponse.ContainsKey(CustomResponseConstants.ResponseKeys.SsoOrganizationIdentifier));
|
||||||
|
Assert.Equal(orgIdentifier, context.CustomResponse[CustomResponseConstants.ResponseKeys.SsoOrganizationIdentifier]);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.Received(1)
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoRequired_NoOrganizationIdentifier_DoesNotIncludeIdentifierInResponse(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
context.User = user;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = true };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id)
|
||||||
|
.Returns((string)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.True(context.SsoRequired);
|
||||||
|
Assert.NotNull(context.CustomResponse);
|
||||||
|
Assert.False(context.CustomResponse.ContainsKey(CustomResponseConstants.ResponseKeys.SsoOrganizationIdentifier));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.Received(1)
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoRequired_EmptyOrganizationIdentifier_DoesNotIncludeIdentifierInResponse(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
context.User = user;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = true };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id)
|
||||||
|
.Returns(string.Empty);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.True(context.SsoRequired);
|
||||||
|
Assert.NotNull(context.CustomResponse);
|
||||||
|
Assert.False(context.CustomResponse.ContainsKey(CustomResponseConstants.ResponseKeys.SsoOrganizationIdentifier));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.Received(1)
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoNotRequired_DoesNotCallOrganizationIdentifierQuery(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = false };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.False(context.SsoRequired);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.DidNotReceive()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ public class TwoFactorAuthenticationValidatorTests
|
|||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmail2faSessionTokenable;
|
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmail2faSessionTokenable;
|
||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorenabledQuery;
|
private readonly ITwoFactorIsEnabledQuery _twoFactorEnabledQuery;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly TwoFactorAuthenticationValidator _sut;
|
private readonly TwoFactorAuthenticationValidator _sut;
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ public class TwoFactorAuthenticationValidatorTests
|
|||||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||||
_ssoEmail2faSessionTokenable = Substitute.For<IDataProtectorTokenFactory<SsoEmail2faSessionTokenable>>();
|
_ssoEmail2faSessionTokenable = Substitute.For<IDataProtectorTokenFactory<SsoEmail2faSessionTokenable>>();
|
||||||
_twoFactorenabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
|
_twoFactorEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
|
||||||
_currentContext = Substitute.For<ICurrentContext>();
|
_currentContext = Substitute.For<ICurrentContext>();
|
||||||
|
|
||||||
_sut = new TwoFactorAuthenticationValidator(
|
_sut = new TwoFactorAuthenticationValidator(
|
||||||
@@ -56,7 +56,7 @@ public class TwoFactorAuthenticationValidatorTests
|
|||||||
_organizationUserRepository,
|
_organizationUserRepository,
|
||||||
_organizationRepository,
|
_organizationRepository,
|
||||||
_ssoEmail2faSessionTokenable,
|
_ssoEmail2faSessionTokenable,
|
||||||
_twoFactorenabledQuery,
|
_twoFactorEnabledQuery,
|
||||||
_currentContext);
|
_currentContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ IBaseRequestValidatorTestWrapper
|
|||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
|
ISsoRequestValidator ssoRequestValidator,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@@ -74,6 +75,7 @@ IBaseRequestValidatorTestWrapper
|
|||||||
eventService,
|
eventService,
|
||||||
deviceValidator,
|
deviceValidator,
|
||||||
twoFactorAuthenticationValidator,
|
twoFactorAuthenticationValidator,
|
||||||
|
ssoRequestValidator,
|
||||||
organizationUserRepository,
|
organizationUserRepository,
|
||||||
logger,
|
logger,
|
||||||
currentContext,
|
currentContext,
|
||||||
@@ -134,12 +136,17 @@ IBaseRequestValidatorTestWrapper
|
|||||||
protected override void SetTwoFactorResult(
|
protected override void SetTwoFactorResult(
|
||||||
BaseRequestValidationContextFake context,
|
BaseRequestValidationContextFake context,
|
||||||
Dictionary<string, object> customResponse)
|
Dictionary<string, object> customResponse)
|
||||||
{ }
|
{
|
||||||
|
context.GrantResult = new GrantValidationResult(
|
||||||
|
TokenRequestErrors.InvalidGrant, "Two-factor authentication required.", customResponse);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void SetValidationErrorResult(
|
protected override void SetValidationErrorResult(
|
||||||
BaseRequestValidationContextFake context,
|
BaseRequestValidationContextFake context,
|
||||||
CustomValidatorRequestContext requestContext)
|
CustomValidatorRequestContext requestContext)
|
||||||
{ }
|
{
|
||||||
|
context.GrantResult.IsError = true;
|
||||||
|
}
|
||||||
|
|
||||||
protected override Task<bool> ValidateContextAsync(
|
protected override Task<bool> ValidateContextAsync(
|
||||||
BaseRequestValidationContextFake context,
|
BaseRequestValidationContextFake context,
|
||||||
|
|||||||
@@ -93,7 +93,8 @@ public static class OrganizationTestHelpers
|
|||||||
UseOrganizationDomains = true,
|
UseOrganizationDomains = true,
|
||||||
UseAdminSponsoredFamilies = true,
|
UseAdminSponsoredFamilies = true,
|
||||||
SyncSeats = false,
|
SyncSeats = false,
|
||||||
UseAutomaticUserConfirmation = true
|
UseAutomaticUserConfirmation = true,
|
||||||
|
UsePhishingBlocker = true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -673,7 +673,8 @@ public class OrganizationUserRepositoryTests
|
|||||||
LimitItemDeletion = false,
|
LimitItemDeletion = false,
|
||||||
AllowAdminAccessToAllCollectionItems = false,
|
AllowAdminAccessToAllCollectionItems = false,
|
||||||
UseRiskInsights = false,
|
UseRiskInsights = false,
|
||||||
UseAdminSponsoredFamilies = false
|
UseAdminSponsoredFamilies = false,
|
||||||
|
UsePhishingBlocker = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
var organizationDomain = new OrganizationDomain
|
var organizationDomain = new OrganizationDomain
|
||||||
|
|||||||
@@ -225,6 +225,30 @@ public class HubHelpersTest
|
|||||||
.Group(Arg.Any<string>());
|
.Group(Arg.Any<string>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task SendNotificationToHubAsync_PolicyChanged_SentToOrganizationGroup(
|
||||||
|
SutProvider<HubHelpers> sutProvider,
|
||||||
|
SyncPolicyPushNotification notification,
|
||||||
|
string contextId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var json = ToNotificationJson(notification, PushType.PolicyChanged, contextId);
|
||||||
|
await sutProvider.Sut.SendNotificationToHubAsync(json, cancellationToken);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(0).User(Arg.Any<string>());
|
||||||
|
await sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(1)
|
||||||
|
.Group($"Organization_{notification.OrganizationId}")
|
||||||
|
.Received(1)
|
||||||
|
.SendCoreAsync("ReceiveMessage", Arg.Is<object?[]>(objects =>
|
||||||
|
objects.Length == 1 && AssertSyncPolicyPushNotification(notification, objects[0],
|
||||||
|
PushType.PolicyChanged, contextId)),
|
||||||
|
cancellationToken);
|
||||||
|
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0).User(Arg.Any<string>());
|
||||||
|
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0)
|
||||||
|
.Group(Arg.Any<string>());
|
||||||
|
}
|
||||||
|
|
||||||
private static string ToNotificationJson(object payload, PushType type, string contextId)
|
private static string ToNotificationJson(object payload, PushType type, string contextId)
|
||||||
{
|
{
|
||||||
var notification = new PushNotificationData<object>(type, payload, contextId);
|
var notification = new PushNotificationData<object>(type, payload, contextId);
|
||||||
@@ -247,4 +271,20 @@ public class HubHelpersTest
|
|||||||
expected.ClientType == pushNotificationData.Payload.ClientType &&
|
expected.ClientType == pushNotificationData.Payload.ClientType &&
|
||||||
expected.RevisionDate == pushNotificationData.Payload.RevisionDate;
|
expected.RevisionDate == pushNotificationData.Payload.RevisionDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool AssertSyncPolicyPushNotification(SyncPolicyPushNotification expected, object? actual,
|
||||||
|
PushType type, string contextId)
|
||||||
|
{
|
||||||
|
if (actual is not PushNotificationData<SyncPolicyPushNotification> pushNotificationData)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pushNotificationData.Type == type &&
|
||||||
|
pushNotificationData.ContextId == contextId &&
|
||||||
|
expected.OrganizationId == pushNotificationData.Payload.OrganizationId &&
|
||||||
|
expected.Policy.Id == pushNotificationData.Payload.Policy.Id &&
|
||||||
|
expected.Policy.Type == pushNotificationData.Payload.Policy.Type &&
|
||||||
|
expected.Policy.Enabled == pushNotificationData.Payload.Policy.Enabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,382 @@
|
|||||||
|
|
||||||
|
/* Introduce new column 'UsePhishingBlocker' not nullable with default of 0 */
|
||||||
|
IF COL_LENGTH('[dbo].[Organization]', 'UsePhishingBlocker') is NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE [dbo].[Organization] ADD [UsePhishingBlocker] bit NOT NULL CONSTRAINT [DF_Organization_UsePhishingBlocker] default (0)
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
/* Update existing stored procedures to include the new column - UsePhishingBlocker */
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Organization_Create]
|
||||||
|
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||||
|
@Identifier NVARCHAR(50),
|
||||||
|
@Name NVARCHAR(50),
|
||||||
|
@BusinessName NVARCHAR(50),
|
||||||
|
@BusinessAddress1 NVARCHAR(50),
|
||||||
|
@BusinessAddress2 NVARCHAR(50),
|
||||||
|
@BusinessAddress3 NVARCHAR(50),
|
||||||
|
@BusinessCountry VARCHAR(2),
|
||||||
|
@BusinessTaxNumber NVARCHAR(30),
|
||||||
|
@BillingEmail NVARCHAR(256),
|
||||||
|
@Plan NVARCHAR(50),
|
||||||
|
@PlanType TINYINT,
|
||||||
|
@Seats INT,
|
||||||
|
@MaxCollections SMALLINT,
|
||||||
|
@UsePolicies BIT,
|
||||||
|
@UseSso BIT,
|
||||||
|
@UseGroups BIT,
|
||||||
|
@UseDirectory BIT,
|
||||||
|
@UseEvents BIT,
|
||||||
|
@UseTotp BIT,
|
||||||
|
@Use2fa BIT,
|
||||||
|
@UseApi BIT,
|
||||||
|
@UseResetPassword BIT,
|
||||||
|
@SelfHost BIT,
|
||||||
|
@UsersGetPremium BIT,
|
||||||
|
@Storage BIGINT,
|
||||||
|
@MaxStorageGb SMALLINT,
|
||||||
|
@Gateway TINYINT,
|
||||||
|
@GatewayCustomerId VARCHAR(50),
|
||||||
|
@GatewaySubscriptionId VARCHAR(50),
|
||||||
|
@ReferenceData VARCHAR(MAX),
|
||||||
|
@Enabled BIT,
|
||||||
|
@LicenseKey VARCHAR(100),
|
||||||
|
@PublicKey VARCHAR(MAX),
|
||||||
|
@PrivateKey VARCHAR(MAX),
|
||||||
|
@TwoFactorProviders NVARCHAR(MAX),
|
||||||
|
@ExpirationDate DATETIME2(7),
|
||||||
|
@CreationDate DATETIME2(7),
|
||||||
|
@RevisionDate DATETIME2(7),
|
||||||
|
@OwnersNotifiedOfAutoscaling DATETIME2(7),
|
||||||
|
@MaxAutoscaleSeats INT,
|
||||||
|
@UseKeyConnector BIT = 0,
|
||||||
|
@UseScim BIT = 0,
|
||||||
|
@UseCustomPermissions BIT = 0,
|
||||||
|
@UseSecretsManager BIT = 0,
|
||||||
|
@Status TINYINT = 0,
|
||||||
|
@UsePasswordManager BIT = 1,
|
||||||
|
@SmSeats INT = null,
|
||||||
|
@SmServiceAccounts INT = null,
|
||||||
|
@MaxAutoscaleSmSeats INT= null,
|
||||||
|
@MaxAutoscaleSmServiceAccounts INT = null,
|
||||||
|
@SecretsManagerBeta BIT = 0,
|
||||||
|
@LimitCollectionCreation BIT = NULL,
|
||||||
|
@LimitCollectionDeletion BIT = NULL,
|
||||||
|
@AllowAdminAccessToAllCollectionItems BIT = 0,
|
||||||
|
@UseRiskInsights BIT = 0,
|
||||||
|
@LimitItemDeletion BIT = 0,
|
||||||
|
@UseOrganizationDomains BIT = 0,
|
||||||
|
@UseAdminSponsoredFamilies BIT = 0,
|
||||||
|
@SyncSeats BIT = 0,
|
||||||
|
@UseAutomaticUserConfirmation BIT = 0,
|
||||||
|
@UsePhishingBlocker BIT = 0
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
INSERT INTO [dbo].[Organization]
|
||||||
|
(
|
||||||
|
[Id],
|
||||||
|
[Identifier],
|
||||||
|
[Name],
|
||||||
|
[BusinessName],
|
||||||
|
[BusinessAddress1],
|
||||||
|
[BusinessAddress2],
|
||||||
|
[BusinessAddress3],
|
||||||
|
[BusinessCountry],
|
||||||
|
[BusinessTaxNumber],
|
||||||
|
[BillingEmail],
|
||||||
|
[Plan],
|
||||||
|
[PlanType],
|
||||||
|
[Seats],
|
||||||
|
[MaxCollections],
|
||||||
|
[UsePolicies],
|
||||||
|
[UseSso],
|
||||||
|
[UseGroups],
|
||||||
|
[UseDirectory],
|
||||||
|
[UseEvents],
|
||||||
|
[UseTotp],
|
||||||
|
[Use2fa],
|
||||||
|
[UseApi],
|
||||||
|
[UseResetPassword],
|
||||||
|
[SelfHost],
|
||||||
|
[UsersGetPremium],
|
||||||
|
[Storage],
|
||||||
|
[MaxStorageGb],
|
||||||
|
[Gateway],
|
||||||
|
[GatewayCustomerId],
|
||||||
|
[GatewaySubscriptionId],
|
||||||
|
[ReferenceData],
|
||||||
|
[Enabled],
|
||||||
|
[LicenseKey],
|
||||||
|
[PublicKey],
|
||||||
|
[PrivateKey],
|
||||||
|
[TwoFactorProviders],
|
||||||
|
[ExpirationDate],
|
||||||
|
[CreationDate],
|
||||||
|
[RevisionDate],
|
||||||
|
[OwnersNotifiedOfAutoscaling],
|
||||||
|
[MaxAutoscaleSeats],
|
||||||
|
[UseKeyConnector],
|
||||||
|
[UseScim],
|
||||||
|
[UseCustomPermissions],
|
||||||
|
[UseSecretsManager],
|
||||||
|
[Status],
|
||||||
|
[UsePasswordManager],
|
||||||
|
[SmSeats],
|
||||||
|
[SmServiceAccounts],
|
||||||
|
[MaxAutoscaleSmSeats],
|
||||||
|
[MaxAutoscaleSmServiceAccounts],
|
||||||
|
[SecretsManagerBeta],
|
||||||
|
[LimitCollectionCreation],
|
||||||
|
[LimitCollectionDeletion],
|
||||||
|
[AllowAdminAccessToAllCollectionItems],
|
||||||
|
[UseRiskInsights],
|
||||||
|
[LimitItemDeletion],
|
||||||
|
[UseOrganizationDomains],
|
||||||
|
[UseAdminSponsoredFamilies],
|
||||||
|
[SyncSeats],
|
||||||
|
[UseAutomaticUserConfirmation],
|
||||||
|
[UsePhishingBlocker]
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@Id,
|
||||||
|
@Identifier,
|
||||||
|
@Name,
|
||||||
|
@BusinessName,
|
||||||
|
@BusinessAddress1,
|
||||||
|
@BusinessAddress2,
|
||||||
|
@BusinessAddress3,
|
||||||
|
@BusinessCountry,
|
||||||
|
@BusinessTaxNumber,
|
||||||
|
@BillingEmail,
|
||||||
|
@Plan,
|
||||||
|
@PlanType,
|
||||||
|
@Seats,
|
||||||
|
@MaxCollections,
|
||||||
|
@UsePolicies,
|
||||||
|
@UseSso,
|
||||||
|
@UseGroups,
|
||||||
|
@UseDirectory,
|
||||||
|
@UseEvents,
|
||||||
|
@UseTotp,
|
||||||
|
@Use2fa,
|
||||||
|
@UseApi,
|
||||||
|
@UseResetPassword,
|
||||||
|
@SelfHost,
|
||||||
|
@UsersGetPremium,
|
||||||
|
@Storage,
|
||||||
|
@MaxStorageGb,
|
||||||
|
@Gateway,
|
||||||
|
@GatewayCustomerId,
|
||||||
|
@GatewaySubscriptionId,
|
||||||
|
@ReferenceData,
|
||||||
|
@Enabled,
|
||||||
|
@LicenseKey,
|
||||||
|
@PublicKey,
|
||||||
|
@PrivateKey,
|
||||||
|
@TwoFactorProviders,
|
||||||
|
@ExpirationDate,
|
||||||
|
@CreationDate,
|
||||||
|
@RevisionDate,
|
||||||
|
@OwnersNotifiedOfAutoscaling,
|
||||||
|
@MaxAutoscaleSeats,
|
||||||
|
@UseKeyConnector,
|
||||||
|
@UseScim,
|
||||||
|
@UseCustomPermissions,
|
||||||
|
@UseSecretsManager,
|
||||||
|
@Status,
|
||||||
|
@UsePasswordManager,
|
||||||
|
@SmSeats,
|
||||||
|
@SmServiceAccounts,
|
||||||
|
@MaxAutoscaleSmSeats,
|
||||||
|
@MaxAutoscaleSmServiceAccounts,
|
||||||
|
@SecretsManagerBeta,
|
||||||
|
@LimitCollectionCreation,
|
||||||
|
@LimitCollectionDeletion,
|
||||||
|
@AllowAdminAccessToAllCollectionItems,
|
||||||
|
@UseRiskInsights,
|
||||||
|
@LimitItemDeletion,
|
||||||
|
@UseOrganizationDomains,
|
||||||
|
@UseAdminSponsoredFamilies,
|
||||||
|
@SyncSeats,
|
||||||
|
@UseAutomaticUserConfirmation,
|
||||||
|
@UsePhishingBlocker
|
||||||
|
);
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
/* Update existing stored procedures to include the new column - UsePhishingBlocker */
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Organization_ReadAbilities]
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
[Id],
|
||||||
|
[UseEvents],
|
||||||
|
[Use2fa],
|
||||||
|
CASE
|
||||||
|
WHEN [Use2fa] = 1 AND [TwoFactorProviders] IS NOT NULL AND [TwoFactorProviders] != '{}' THEN
|
||||||
|
1
|
||||||
|
ELSE
|
||||||
|
0
|
||||||
|
END AS [Using2fa],
|
||||||
|
[UsersGetPremium],
|
||||||
|
[UseCustomPermissions],
|
||||||
|
[UseSso],
|
||||||
|
[UseKeyConnector],
|
||||||
|
[UseScim],
|
||||||
|
[UseResetPassword],
|
||||||
|
[UsePolicies],
|
||||||
|
[Enabled],
|
||||||
|
[LimitCollectionCreation],
|
||||||
|
[LimitCollectionDeletion],
|
||||||
|
[AllowAdminAccessToAllCollectionItems],
|
||||||
|
[UseRiskInsights],
|
||||||
|
[LimitItemDeletion],
|
||||||
|
[UseOrganizationDomains],
|
||||||
|
[UseAdminSponsoredFamilies],
|
||||||
|
[UseAutomaticUserConfirmation],
|
||||||
|
[UsePhishingBlocker]
|
||||||
|
FROM
|
||||||
|
[dbo].[Organization]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
/* Update existing stored procedures to include the new column - UsePhishingBlocker */
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Organization_Update]
|
||||||
|
@Id UNIQUEIDENTIFIER,
|
||||||
|
@Identifier NVARCHAR(50),
|
||||||
|
@Name NVARCHAR(50),
|
||||||
|
@BusinessName NVARCHAR(50),
|
||||||
|
@BusinessAddress1 NVARCHAR(50),
|
||||||
|
@BusinessAddress2 NVARCHAR(50),
|
||||||
|
@BusinessAddress3 NVARCHAR(50),
|
||||||
|
@BusinessCountry VARCHAR(2),
|
||||||
|
@BusinessTaxNumber NVARCHAR(30),
|
||||||
|
@BillingEmail NVARCHAR(256),
|
||||||
|
@Plan NVARCHAR(50),
|
||||||
|
@PlanType TINYINT,
|
||||||
|
@Seats INT,
|
||||||
|
@MaxCollections SMALLINT,
|
||||||
|
@UsePolicies BIT,
|
||||||
|
@UseSso BIT,
|
||||||
|
@UseGroups BIT,
|
||||||
|
@UseDirectory BIT,
|
||||||
|
@UseEvents BIT,
|
||||||
|
@UseTotp BIT,
|
||||||
|
@Use2fa BIT,
|
||||||
|
@UseApi BIT,
|
||||||
|
@UseResetPassword BIT,
|
||||||
|
@SelfHost BIT,
|
||||||
|
@UsersGetPremium BIT,
|
||||||
|
@Storage BIGINT,
|
||||||
|
@MaxStorageGb SMALLINT,
|
||||||
|
@Gateway TINYINT,
|
||||||
|
@GatewayCustomerId VARCHAR(50),
|
||||||
|
@GatewaySubscriptionId VARCHAR(50),
|
||||||
|
@ReferenceData VARCHAR(MAX),
|
||||||
|
@Enabled BIT,
|
||||||
|
@LicenseKey VARCHAR(100),
|
||||||
|
@PublicKey VARCHAR(MAX),
|
||||||
|
@PrivateKey VARCHAR(MAX),
|
||||||
|
@TwoFactorProviders NVARCHAR(MAX),
|
||||||
|
@ExpirationDate DATETIME2(7),
|
||||||
|
@CreationDate DATETIME2(7),
|
||||||
|
@RevisionDate DATETIME2(7),
|
||||||
|
@OwnersNotifiedOfAutoscaling DATETIME2(7),
|
||||||
|
@MaxAutoscaleSeats INT,
|
||||||
|
@UseKeyConnector BIT = 0,
|
||||||
|
@UseScim BIT = 0,
|
||||||
|
@UseCustomPermissions BIT = 0,
|
||||||
|
@UseSecretsManager BIT = 0,
|
||||||
|
@Status TINYINT = 0,
|
||||||
|
@UsePasswordManager BIT = 1,
|
||||||
|
@SmSeats INT = null,
|
||||||
|
@SmServiceAccounts INT = null,
|
||||||
|
@MaxAutoscaleSmSeats INT = null,
|
||||||
|
@MaxAutoscaleSmServiceAccounts INT = null,
|
||||||
|
@SecretsManagerBeta BIT = 0,
|
||||||
|
@LimitCollectionCreation BIT = null,
|
||||||
|
@LimitCollectionDeletion BIT = null,
|
||||||
|
@AllowAdminAccessToAllCollectionItems BIT = 0,
|
||||||
|
@UseRiskInsights BIT = 0,
|
||||||
|
@LimitItemDeletion BIT = 0,
|
||||||
|
@UseOrganizationDomains BIT = 0,
|
||||||
|
@UseAdminSponsoredFamilies BIT = 0,
|
||||||
|
@SyncSeats BIT = 0,
|
||||||
|
@UseAutomaticUserConfirmation BIT = 0,
|
||||||
|
@UsePhishingBlocker BIT = 0
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
UPDATE
|
||||||
|
[dbo].[Organization]
|
||||||
|
SET
|
||||||
|
[Identifier] = @Identifier,
|
||||||
|
[Name] = @Name,
|
||||||
|
[BusinessName] = @BusinessName,
|
||||||
|
[BusinessAddress1] = @BusinessAddress1,
|
||||||
|
[BusinessAddress2] = @BusinessAddress2,
|
||||||
|
[BusinessAddress3] = @BusinessAddress3,
|
||||||
|
[BusinessCountry] = @BusinessCountry,
|
||||||
|
[BusinessTaxNumber] = @BusinessTaxNumber,
|
||||||
|
[BillingEmail] = @BillingEmail,
|
||||||
|
[Plan] = @Plan,
|
||||||
|
[PlanType] = @PlanType,
|
||||||
|
[Seats] = @Seats,
|
||||||
|
[MaxCollections] = @MaxCollections,
|
||||||
|
[UsePolicies] = @UsePolicies,
|
||||||
|
[UseSso] = @UseSso,
|
||||||
|
[UseGroups] = @UseGroups,
|
||||||
|
[UseDirectory] = @UseDirectory,
|
||||||
|
[UseEvents] = @UseEvents,
|
||||||
|
[UseTotp] = @UseTotp,
|
||||||
|
[Use2fa] = @Use2fa,
|
||||||
|
[UseApi] = @UseApi,
|
||||||
|
[UseResetPassword] = @UseResetPassword,
|
||||||
|
[SelfHost] = @SelfHost,
|
||||||
|
[UsersGetPremium] = @UsersGetPremium,
|
||||||
|
[Storage] = @Storage,
|
||||||
|
[MaxStorageGb] = @MaxStorageGb,
|
||||||
|
[Gateway] = @Gateway,
|
||||||
|
[GatewayCustomerId] = @GatewayCustomerId,
|
||||||
|
[GatewaySubscriptionId] = @GatewaySubscriptionId,
|
||||||
|
[ReferenceData] = @ReferenceData,
|
||||||
|
[Enabled] = @Enabled,
|
||||||
|
[LicenseKey] = @LicenseKey,
|
||||||
|
[PublicKey] = @PublicKey,
|
||||||
|
[PrivateKey] = @PrivateKey,
|
||||||
|
[TwoFactorProviders] = @TwoFactorProviders,
|
||||||
|
[ExpirationDate] = @ExpirationDate,
|
||||||
|
[CreationDate] = @CreationDate,
|
||||||
|
[RevisionDate] = @RevisionDate,
|
||||||
|
[OwnersNotifiedOfAutoscaling] = @OwnersNotifiedOfAutoscaling,
|
||||||
|
[MaxAutoscaleSeats] = @MaxAutoscaleSeats,
|
||||||
|
[UseKeyConnector] = @UseKeyConnector,
|
||||||
|
[UseScim] = @UseScim,
|
||||||
|
[UseCustomPermissions] = @UseCustomPermissions,
|
||||||
|
[UseSecretsManager] = @UseSecretsManager,
|
||||||
|
[Status] = @Status,
|
||||||
|
[UsePasswordManager] = @UsePasswordManager,
|
||||||
|
[SmSeats] = @SmSeats,
|
||||||
|
[SmServiceAccounts] = @SmServiceAccounts,
|
||||||
|
[MaxAutoscaleSmSeats] = @MaxAutoscaleSmSeats,
|
||||||
|
[MaxAutoscaleSmServiceAccounts] = @MaxAutoscaleSmServiceAccounts,
|
||||||
|
[SecretsManagerBeta] = @SecretsManagerBeta,
|
||||||
|
[LimitCollectionCreation] = @LimitCollectionCreation,
|
||||||
|
[LimitCollectionDeletion] = @LimitCollectionDeletion,
|
||||||
|
[AllowAdminAccessToAllCollectionItems] = @AllowAdminAccessToAllCollectionItems,
|
||||||
|
[UseRiskInsights] = @UseRiskInsights,
|
||||||
|
[LimitItemDeletion] = @LimitItemDeletion,
|
||||||
|
[UseOrganizationDomains] = @UseOrganizationDomains,
|
||||||
|
[UseAdminSponsoredFamilies] = @UseAdminSponsoredFamilies,
|
||||||
|
[SyncSeats] = @SyncSeats,
|
||||||
|
[UseAutomaticUserConfirmation] = @UseAutomaticUserConfirmation,
|
||||||
|
[UsePhishingBlocker] = @UsePhishingBlocker
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id;
|
||||||
|
END
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
/* Adds the UsePhishingBlocker column to the OrganizationUserOrganizationDetailsView view. */
|
||||||
|
CREATE OR ALTER VIEW [dbo].[OrganizationUserOrganizationDetailsView]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
OU.[UserId],
|
||||||
|
OU.[OrganizationId],
|
||||||
|
OU.[Id] OrganizationUserId,
|
||||||
|
O.[Name],
|
||||||
|
O.[Enabled],
|
||||||
|
O.[PlanType],
|
||||||
|
O.[UsePolicies],
|
||||||
|
O.[UseSso],
|
||||||
|
O.[UseKeyConnector],
|
||||||
|
O.[UseScim],
|
||||||
|
O.[UseGroups],
|
||||||
|
O.[UseDirectory],
|
||||||
|
O.[UseEvents],
|
||||||
|
O.[UseTotp],
|
||||||
|
O.[Use2fa],
|
||||||
|
O.[UseApi],
|
||||||
|
O.[UseResetPassword],
|
||||||
|
O.[SelfHost],
|
||||||
|
O.[UsersGetPremium],
|
||||||
|
O.[UseCustomPermissions],
|
||||||
|
O.[UseSecretsManager],
|
||||||
|
O.[Seats],
|
||||||
|
O.[MaxCollections],
|
||||||
|
COALESCE(O.[MaxStorageGbIncreased], O.[MaxStorageGb]) AS [MaxStorageGb],
|
||||||
|
O.[Identifier],
|
||||||
|
OU.[Key],
|
||||||
|
OU.[ResetPasswordKey],
|
||||||
|
O.[PublicKey],
|
||||||
|
O.[PrivateKey],
|
||||||
|
OU.[Status],
|
||||||
|
OU.[Type],
|
||||||
|
SU.[ExternalId] SsoExternalId,
|
||||||
|
OU.[Permissions],
|
||||||
|
PO.[ProviderId],
|
||||||
|
P.[Name] ProviderName,
|
||||||
|
P.[Type] ProviderType,
|
||||||
|
SS.[Enabled] SsoEnabled,
|
||||||
|
SS.[Data] SsoConfig,
|
||||||
|
OS.[FriendlyName] FamilySponsorshipFriendlyName,
|
||||||
|
OS.[LastSyncDate] FamilySponsorshipLastSyncDate,
|
||||||
|
OS.[ToDelete] FamilySponsorshipToDelete,
|
||||||
|
OS.[ValidUntil] FamilySponsorshipValidUntil,
|
||||||
|
OU.[AccessSecretsManager],
|
||||||
|
O.[UsePasswordManager],
|
||||||
|
O.[SmSeats],
|
||||||
|
O.[SmServiceAccounts],
|
||||||
|
O.[LimitCollectionCreation],
|
||||||
|
O.[LimitCollectionDeletion],
|
||||||
|
O.[AllowAdminAccessToAllCollectionItems],
|
||||||
|
O.[UseRiskInsights],
|
||||||
|
O.[LimitItemDeletion],
|
||||||
|
O.[UseAdminSponsoredFamilies],
|
||||||
|
O.[UseOrganizationDomains],
|
||||||
|
OS.[IsAdminInitiated],
|
||||||
|
O.[UseAutomaticUserConfirmation],
|
||||||
|
O.[UsePhishingBlocker]
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationUser] OU
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[Organization] O ON O.[Id] = OU.[OrganizationId]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[Provider] P ON P.[Id] = PO.[ProviderId]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserID] = OU.[Id]
|
||||||
|
GO
|
||||||
|
|
||||||
|
/* Updates the ProviderUserProviderOrganizationDetailsView view to include the UsePhishingBlocker column. */
|
||||||
|
CREATE OR ALTER VIEW [dbo].[ProviderUserProviderOrganizationDetailsView]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
PU.[UserId],
|
||||||
|
PO.[OrganizationId],
|
||||||
|
O.[Name],
|
||||||
|
O.[Enabled],
|
||||||
|
O.[UsePolicies],
|
||||||
|
O.[UseSso],
|
||||||
|
O.[UseKeyConnector],
|
||||||
|
O.[UseScim],
|
||||||
|
O.[UseGroups],
|
||||||
|
O.[UseDirectory],
|
||||||
|
O.[UseEvents],
|
||||||
|
O.[UseTotp],
|
||||||
|
O.[Use2fa],
|
||||||
|
O.[UseApi],
|
||||||
|
O.[UseResetPassword],
|
||||||
|
O.[UseSecretsManager],
|
||||||
|
O.[UsePasswordManager],
|
||||||
|
O.[SelfHost],
|
||||||
|
O.[UsersGetPremium],
|
||||||
|
O.[UseCustomPermissions],
|
||||||
|
O.[Seats],
|
||||||
|
O.[MaxCollections],
|
||||||
|
COALESCE(O.[MaxStorageGbIncreased], O.[MaxStorageGb]) AS [MaxStorageGb],
|
||||||
|
O.[Identifier],
|
||||||
|
PO.[Key],
|
||||||
|
O.[PublicKey],
|
||||||
|
O.[PrivateKey],
|
||||||
|
PU.[Status],
|
||||||
|
PU.[Type],
|
||||||
|
PO.[ProviderId],
|
||||||
|
PU.[Id] ProviderUserId,
|
||||||
|
P.[Name] ProviderName,
|
||||||
|
O.[PlanType],
|
||||||
|
O.[LimitCollectionCreation],
|
||||||
|
O.[LimitCollectionDeletion],
|
||||||
|
O.[AllowAdminAccessToAllCollectionItems],
|
||||||
|
O.[UseRiskInsights],
|
||||||
|
O.[UseAdminSponsoredFamilies],
|
||||||
|
P.[Type] ProviderType,
|
||||||
|
O.[LimitItemDeletion],
|
||||||
|
O.[UseOrganizationDomains],
|
||||||
|
O.[UseAutomaticUserConfirmation],
|
||||||
|
SS.[Enabled] SsoEnabled,
|
||||||
|
SS.[Data] SsoConfig,
|
||||||
|
O.[UsePhishingBlocker]
|
||||||
|
FROM
|
||||||
|
[dbo].[ProviderUser] PU
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[ProviderOrganization] PO ON PO.[ProviderId] = PU.[ProviderId]
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[Organization] O ON O.[Id] = PO.[OrganizationId]
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[Provider] P ON P.[Id] = PU.[ProviderId]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[SsoConfig] SS ON SS.[OrganizationId] = O.[Id]
|
||||||
|
GO
|
||||||
|
|
||||||
|
/* Updates the OrganizationView view to include the UsePhishingBlocker column. */
|
||||||
|
CREATE OR ALTER VIEW [dbo].[OrganizationView]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
[Id],
|
||||||
|
[Identifier],
|
||||||
|
[Name],
|
||||||
|
[BusinessName],
|
||||||
|
[BusinessAddress1],
|
||||||
|
[BusinessAddress2],
|
||||||
|
[BusinessAddress3],
|
||||||
|
[BusinessCountry],
|
||||||
|
[BusinessTaxNumber],
|
||||||
|
[BillingEmail],
|
||||||
|
[Plan],
|
||||||
|
[PlanType],
|
||||||
|
[Seats],
|
||||||
|
[MaxCollections],
|
||||||
|
[UsePolicies],
|
||||||
|
[UseSso],
|
||||||
|
[UseGroups],
|
||||||
|
[UseDirectory],
|
||||||
|
[UseEvents],
|
||||||
|
[UseTotp],
|
||||||
|
[Use2fa],
|
||||||
|
[UseApi],
|
||||||
|
[UseResetPassword],
|
||||||
|
[SelfHost],
|
||||||
|
[UsersGetPremium],
|
||||||
|
[Storage],
|
||||||
|
COALESCE([MaxStorageGbIncreased], [MaxStorageGb]) AS [MaxStorageGb],
|
||||||
|
[Gateway],
|
||||||
|
[GatewayCustomerId],
|
||||||
|
[GatewaySubscriptionId],
|
||||||
|
[ReferenceData],
|
||||||
|
[Enabled],
|
||||||
|
[LicenseKey],
|
||||||
|
[PublicKey],
|
||||||
|
[PrivateKey],
|
||||||
|
[TwoFactorProviders],
|
||||||
|
[ExpirationDate],
|
||||||
|
[CreationDate],
|
||||||
|
[RevisionDate],
|
||||||
|
[OwnersNotifiedOfAutoscaling],
|
||||||
|
[MaxAutoscaleSeats],
|
||||||
|
[UseKeyConnector],
|
||||||
|
[UseScim],
|
||||||
|
[UseCustomPermissions],
|
||||||
|
[UseSecretsManager],
|
||||||
|
[Status],
|
||||||
|
[UsePasswordManager],
|
||||||
|
[SmSeats],
|
||||||
|
[SmServiceAccounts],
|
||||||
|
[MaxAutoscaleSmSeats],
|
||||||
|
[MaxAutoscaleSmServiceAccounts],
|
||||||
|
[SecretsManagerBeta],
|
||||||
|
[LimitCollectionCreation],
|
||||||
|
[LimitCollectionDeletion],
|
||||||
|
[LimitItemDeletion],
|
||||||
|
[AllowAdminAccessToAllCollectionItems],
|
||||||
|
[UseRiskInsights],
|
||||||
|
[UseOrganizationDomains],
|
||||||
|
[UseAdminSponsoredFamilies],
|
||||||
|
[SyncSeats],
|
||||||
|
[UseAutomaticUserConfirmation],
|
||||||
|
[UsePhishingBlocker]
|
||||||
|
FROM
|
||||||
|
[dbo].[Organization]
|
||||||
|
GO
|
||||||
|
|
||||||
|
|
||||||
|
--Manually refresh [dbo].[OrganizationUserOrganizationDetailsView]
|
||||||
|
IF OBJECT_ID('[dbo].[OrganizationUserOrganizationDetailsView]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUserOrganizationDetailsView]';
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
--Manually refresh [dbo].[ProviderUserProviderOrganizationDetailsView]
|
||||||
|
IF OBJECT_ID('[dbo].[ProviderUserProviderOrganizationDetailsView]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
EXECUTE sp_refreshsqlmodule N'[dbo].[ProviderUserProviderOrganizationDetailsView]';
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
--Manually refresh [dbo].[OrganizationView]
|
||||||
|
IF OBJECT_ID('[dbo].[OrganizationView]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationView]';
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
--Manually refresh [dbo].[OrganizationCipherDetailsCollectionsView]
|
||||||
|
IF OBJECT_ID('[dbo].[OrganizationCipherDetailsCollectionsView]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationCipherDetailsCollectionsView]';
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
--Manually refresh [dbo].[ProviderOrganizationOrganizationDetailsView]
|
||||||
|
IF OBJECT_ID('[dbo].[ProviderOrganizationOrganizationDetailsView]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
EXECUTE sp_refreshsqlmodule N'[dbo].[ProviderOrganizationOrganizationDetailsView]';
|
||||||
|
END
|
||||||
|
GO
|
||||||
3443
util/MySqlMigrations/Migrations/20251121193008_2025-11-21_00_AddUsePhishingBlockerToOrganization.Designer.cs
generated
Normal file
3443
util/MySqlMigrations/Migrations/20251121193008_2025-11-21_00_AddUsePhishingBlockerToOrganization.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.MySqlMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class _20251121_00_AddUsePhishingBlockerToOrganization : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "UsePhishingBlocker",
|
||||||
|
table: "Organization",
|
||||||
|
type: "tinyint(1)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "UsePhishingBlocker",
|
||||||
|
table: "Organization");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -244,6 +244,9 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
b.Property<bool>("UsePasswordManager")
|
b.Property<bool>("UsePasswordManager")
|
||||||
.HasColumnType("tinyint(1)");
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("UsePhishingBlocker")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
b.Property<bool>("UsePolicies")
|
b.Property<bool>("UsePolicies")
|
||||||
.HasColumnType("tinyint(1)");
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.PostgresMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class _20251121_00_AddUsePhishingBlockerToOrganization : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "UsePhishingBlocker",
|
||||||
|
table: "Organization",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "UsePhishingBlocker",
|
||||||
|
table: "Organization");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -246,6 +246,9 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
b.Property<bool>("UsePasswordManager")
|
b.Property<bool>("UsePasswordManager")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("UsePhishingBlocker")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<bool>("UsePolicies")
|
b.Property<bool>("UsePolicies")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.SqliteMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class _20251121_00_AddUsePhishingBlockerToOrganization : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "UsePhishingBlocker",
|
||||||
|
table: "Organization",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "UsePhishingBlocker",
|
||||||
|
table: "Organization");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -239,6 +239,9 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
b.Property<bool>("UsePasswordManager")
|
b.Property<bool>("UsePasswordManager")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<bool>("UsePhishingBlocker")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<bool>("UsePolicies")
|
b.Property<bool>("UsePolicies")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user