mirror of
https://github.com/bitwarden/server.git
synced 2025-02-17 02:01:53 +01:00
Merge branch 'main' into option-to-disable-built-in-mssql
This commit is contained in:
commit
1d87ba3a46
56
.github/workflows/repository-management.yml
vendored
56
.github/workflows/repository-management.yml
vendored
@ -135,13 +135,61 @@ jobs:
|
||||
git config --local user.email "actions@github.com"
|
||||
git config --local user.name "Github Actions"
|
||||
|
||||
- name: Create version branch
|
||||
id: create-branch
|
||||
run: |
|
||||
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
|
||||
git switch -c $NAME
|
||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit files
|
||||
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
|
||||
|
||||
- name: Push changes
|
||||
run: git push
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
|
||||
- name: Create version PR
|
||||
id: create-pr
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
|
||||
run: |
|
||||
git pull -pt
|
||||
git push
|
||||
PR_URL=$(gh pr create --title "$TITLE" \
|
||||
--base "main" \
|
||||
--head "$PR_BRANCH" \
|
||||
--label "version update" \
|
||||
--label "automated pr" \
|
||||
--body "
|
||||
## Type of change
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature development
|
||||
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
|
||||
- [ ] Build/deploy pipeline (DevOps)
|
||||
- [X] Other
|
||||
## Objective
|
||||
Automated version bump to ${{ steps.set-final-version-output.outputs.version }}")
|
||||
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Approve PR
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||
run: gh pr review $PR_NUMBER --approve
|
||||
|
||||
- name: Merge PR
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
||||
|
||||
|
||||
cherry_pick:
|
||||
@ -153,7 +201,7 @@ jobs:
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
with:
|
||||
ref: main
|
||||
|
||||
|
||||
- name: Install xmllint
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@ -189,7 +237,7 @@ jobs:
|
||||
RC_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
|
||||
echo "rc_version=$RC_VERSION" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --local user.email "actions@github.com"
|
||||
|
@ -40,6 +40,36 @@ public class CreateProviderCommand : ICreateProviderCommand
|
||||
}
|
||||
|
||||
public async Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats)
|
||||
{
|
||||
var providerId = await CreateProviderAsync(provider, ownerEmail);
|
||||
|
||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
if (isConsolidatedBillingEnabled)
|
||||
{
|
||||
await CreateProviderPlanAsync(providerId, PlanType.TeamsMonthly, teamsMinimumSeats);
|
||||
await CreateProviderPlanAsync(providerId, PlanType.EnterpriseMonthly, enterpriseMinimumSeats);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateResellerAsync(Provider provider)
|
||||
{
|
||||
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Created);
|
||||
}
|
||||
|
||||
public async Task CreateMultiOrganizationEnterpriseAsync(Provider provider, string ownerEmail, PlanType plan, int minimumSeats)
|
||||
{
|
||||
var providerId = await CreateProviderAsync(provider, ownerEmail);
|
||||
|
||||
var isConsolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling);
|
||||
|
||||
if (isConsolidatedBillingEnabled)
|
||||
{
|
||||
await CreateProviderPlanAsync(providerId, plan, minimumSeats);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Guid> CreateProviderAsync(Provider provider, string ownerEmail)
|
||||
{
|
||||
var owner = await _userRepository.GetByEmailAsync(ownerEmail);
|
||||
if (owner == null)
|
||||
@ -64,27 +94,10 @@ public class CreateProviderCommand : ICreateProviderCommand
|
||||
Status = ProviderUserStatusType.Confirmed,
|
||||
};
|
||||
|
||||
if (isConsolidatedBillingEnabled)
|
||||
{
|
||||
var providerPlans = new List<ProviderPlan>
|
||||
{
|
||||
CreateProviderPlan(provider.Id, PlanType.TeamsMonthly, teamsMinimumSeats),
|
||||
CreateProviderPlan(provider.Id, PlanType.EnterpriseMonthly, enterpriseMinimumSeats)
|
||||
};
|
||||
|
||||
foreach (var providerPlan in providerPlans)
|
||||
{
|
||||
await _providerPlanRepository.CreateAsync(providerPlan);
|
||||
}
|
||||
}
|
||||
|
||||
await _providerUserRepository.CreateAsync(providerUser);
|
||||
await _providerService.SendProviderSetupInviteEmailAsync(provider, owner.Email);
|
||||
}
|
||||
|
||||
public async Task CreateResellerAsync(Provider provider)
|
||||
{
|
||||
await ProviderRepositoryCreateAsync(provider, ProviderStatusType.Created);
|
||||
return provider.Id;
|
||||
}
|
||||
|
||||
private async Task ProviderRepositoryCreateAsync(Provider provider, ProviderStatusType status)
|
||||
@ -95,9 +108,9 @@ public class CreateProviderCommand : ICreateProviderCommand
|
||||
await _providerRepository.CreateAsync(provider);
|
||||
}
|
||||
|
||||
private ProviderPlan CreateProviderPlan(Guid providerId, PlanType planType, int seatMinimum)
|
||||
private async Task CreateProviderPlanAsync(Guid providerId, PlanType planType, int seatMinimum)
|
||||
{
|
||||
return new ProviderPlan
|
||||
var plan = new ProviderPlan
|
||||
{
|
||||
ProviderId = providerId,
|
||||
PlanType = planType,
|
||||
@ -105,5 +118,6 @@ public class CreateProviderCommand : ICreateProviderCommand
|
||||
PurchasedSeats = 0,
|
||||
AllocatedSeats = 0
|
||||
};
|
||||
await _providerPlanRepository.CreateAsync(plan);
|
||||
}
|
||||
}
|
||||
|
@ -379,42 +379,23 @@ public class ProviderBillingService(
|
||||
|
||||
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>();
|
||||
|
||||
var teamsProviderPlan =
|
||||
providerPlans.SingleOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly);
|
||||
|
||||
if (teamsProviderPlan == null || !teamsProviderPlan.IsConfigured())
|
||||
foreach (var providerPlan in providerPlans)
|
||||
{
|
||||
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured Teams plan", provider.Id);
|
||||
var plan = StaticStore.GetPlan(providerPlan.PlanType);
|
||||
|
||||
throw new BillingException();
|
||||
if (!providerPlan.IsConfigured())
|
||||
{
|
||||
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured {ProviderName} plan", provider.Id, plan.Name);
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = plan.PasswordManager.StripeProviderPortalSeatPlanId,
|
||||
Quantity = providerPlan.SeatMinimum
|
||||
});
|
||||
}
|
||||
|
||||
var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = teamsPlan.PasswordManager.StripeProviderPortalSeatPlanId,
|
||||
Quantity = teamsProviderPlan.SeatMinimum
|
||||
});
|
||||
|
||||
var enterpriseProviderPlan =
|
||||
providerPlans.SingleOrDefault(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly);
|
||||
|
||||
if (enterpriseProviderPlan == null || !enterpriseProviderPlan.IsConfigured())
|
||||
{
|
||||
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured Enterprise plan", provider.Id);
|
||||
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
|
||||
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = enterprisePlan.PasswordManager.StripeProviderPortalSeatPlanId,
|
||||
Quantity = enterpriseProviderPlan.SeatMinimum
|
||||
});
|
||||
|
||||
var subscriptionCreateOptions = new SubscriptionCreateOptions
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTaxOptions
|
||||
|
@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
@ -19,23 +20,30 @@ public class CreateProviderCommandTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateMspAsync_UserIdIsInvalid_Throws(Provider provider, SutProvider<CreateProviderCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
provider.Type = ProviderType.Msp;
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.CreateMspAsync(provider, default, default, default));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Invalid owner.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateMspAsync_Success(Provider provider, User user, SutProvider<CreateProviderCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
provider.Type = ProviderType.Msp;
|
||||
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
userRepository.GetByEmailAsync(user.Email).Returns(user);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.CreateMspAsync(provider, user.Email, default, default);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
||||
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);
|
||||
}
|
||||
@ -43,11 +51,52 @@ public class CreateProviderCommandTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateResellerAsync_Success(Provider provider, SutProvider<CreateProviderCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
provider.Type = ProviderType.Reseller;
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.CreateResellerAsync(provider);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(default);
|
||||
await sutProvider.GetDependency<IProviderService>().DidNotReceiveWithAnyArgs().SendProviderSetupInviteEmailAsync(default, default);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateMultiOrganizationEnterpriseAsync_Success(
|
||||
Provider provider,
|
||||
User user,
|
||||
PlanType plan,
|
||||
int minimumSeats,
|
||||
SutProvider<CreateProviderCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
provider.Type = ProviderType.MultiOrganizationEnterprise;
|
||||
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
userRepository.GetByEmailAsync(user.Email).Returns(user);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.CreateMultiOrganizationEnterpriseAsync(provider, user.Email, plan, minimumSeats);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IProviderRepository>().ReceivedWithAnyArgs().CreateAsync(provider);
|
||||
await sutProvider.GetDependency<IProviderService>().Received(1).SendProviderSetupInviteEmailAsync(provider, user.Email);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateMultiOrganizationEnterpriseAsync_UserIdIsInvalid_Throws(
|
||||
Provider provider,
|
||||
SutProvider<CreateProviderCommand> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
provider.Type = ProviderType.Msp;
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.CreateMultiOrganizationEnterpriseAsync(provider, default, default, default));
|
||||
|
||||
// Assert
|
||||
Assert.Contains("Invalid owner.", exception.Message);
|
||||
}
|
||||
}
|
||||
|
@ -107,9 +107,15 @@ public class ProvidersController : Controller
|
||||
});
|
||||
}
|
||||
|
||||
public IActionResult Create(int teamsMinimumSeats, int enterpriseMinimumSeats, string ownerEmail = null)
|
||||
public IActionResult Create()
|
||||
{
|
||||
return View(new CreateProviderModel
|
||||
return View(new CreateProviderModel());
|
||||
}
|
||||
|
||||
[HttpGet("providers/create/msp")]
|
||||
public IActionResult CreateMsp(int teamsMinimumSeats, int enterpriseMinimumSeats, string ownerEmail = null)
|
||||
{
|
||||
return View(new CreateMspProviderModel
|
||||
{
|
||||
OwnerEmail = ownerEmail,
|
||||
TeamsMonthlySeatMinimum = teamsMinimumSeats,
|
||||
@ -117,10 +123,50 @@ public class ProvidersController : Controller
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("providers/create/reseller")]
|
||||
public IActionResult CreateReseller()
|
||||
{
|
||||
return View(new CreateResellerProviderModel());
|
||||
}
|
||||
|
||||
[HttpGet("providers/create/multi-organization-enterprise")]
|
||||
public IActionResult CreateMultiOrganizationEnterprise(int enterpriseMinimumSeats, string ownerEmail = null)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises))
|
||||
{
|
||||
return RedirectToAction("Create");
|
||||
}
|
||||
|
||||
return View(new CreateMultiOrganizationEnterpriseProviderModel
|
||||
{
|
||||
OwnerEmail = ownerEmail,
|
||||
EnterpriseSeatMinimum = enterpriseMinimumSeats
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.Provider_Create)]
|
||||
public async Task<IActionResult> Create(CreateProviderModel model)
|
||||
public IActionResult Create(CreateProviderModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
return model.Type switch
|
||||
{
|
||||
ProviderType.Msp => RedirectToAction("CreateMsp"),
|
||||
ProviderType.Reseller => RedirectToAction("CreateReseller"),
|
||||
ProviderType.MultiOrganizationEnterprise => RedirectToAction("CreateMultiOrganizationEnterprise"),
|
||||
_ => View(model)
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("providers/create/msp")]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.Provider_Create)]
|
||||
public async Task<IActionResult> CreateMsp(CreateMspProviderModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
@ -128,19 +174,51 @@ public class ProvidersController : Controller
|
||||
}
|
||||
|
||||
var provider = model.ToProvider();
|
||||
switch (provider.Type)
|
||||
|
||||
await _createProviderCommand.CreateMspAsync(
|
||||
provider,
|
||||
model.OwnerEmail,
|
||||
model.TeamsMonthlySeatMinimum,
|
||||
model.EnterpriseMonthlySeatMinimum);
|
||||
|
||||
return RedirectToAction("Edit", new { id = provider.Id });
|
||||
}
|
||||
|
||||
[HttpPost("providers/create/reseller")]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.Provider_Create)]
|
||||
public async Task<IActionResult> CreateReseller(CreateResellerProviderModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
case ProviderType.Msp:
|
||||
await _createProviderCommand.CreateMspAsync(
|
||||
provider,
|
||||
model.OwnerEmail,
|
||||
model.TeamsMonthlySeatMinimum,
|
||||
model.EnterpriseMonthlySeatMinimum);
|
||||
break;
|
||||
case ProviderType.Reseller:
|
||||
await _createProviderCommand.CreateResellerAsync(provider);
|
||||
break;
|
||||
return View(model);
|
||||
}
|
||||
var provider = model.ToProvider();
|
||||
await _createProviderCommand.CreateResellerAsync(provider);
|
||||
|
||||
return RedirectToAction("Edit", new { id = provider.Id });
|
||||
}
|
||||
|
||||
[HttpPost("providers/create/multi-organization-enterprise")]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.Provider_Create)]
|
||||
public async Task<IActionResult> CreateMultiOrganizationEnterprise(CreateMultiOrganizationEnterpriseProviderModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
var provider = model.ToProvider();
|
||||
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises))
|
||||
{
|
||||
return RedirectToAction("Create");
|
||||
}
|
||||
await _createProviderCommand.CreateMultiOrganizationEnterpriseAsync(
|
||||
provider,
|
||||
model.OwnerEmail,
|
||||
model.Plan.Value,
|
||||
model.EnterpriseSeatMinimum);
|
||||
|
||||
return RedirectToAction("Edit", new { id = provider.Id });
|
||||
}
|
||||
|
45
src/Admin/AdminConsole/Models/CreateMspProviderModel.cs
Normal file
45
src/Admin/AdminConsole/Models/CreateMspProviderModel.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
public class CreateMspProviderModel : IValidatableObject
|
||||
{
|
||||
[Display(Name = "Owner Email")]
|
||||
public string OwnerEmail { get; set; }
|
||||
|
||||
[Display(Name = "Teams (Monthly) Seat Minimum")]
|
||||
public int TeamsMonthlySeatMinimum { get; set; }
|
||||
|
||||
[Display(Name = "Enterprise (Monthly) Seat Minimum")]
|
||||
public int EnterpriseMonthlySeatMinimum { get; set; }
|
||||
|
||||
public virtual Provider ToProvider()
|
||||
{
|
||||
return new Provider
|
||||
{
|
||||
Type = ProviderType.Msp
|
||||
};
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(OwnerEmail))
|
||||
{
|
||||
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateMspProviderModel>()?.GetName() ?? nameof(OwnerEmail);
|
||||
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
|
||||
}
|
||||
if (TeamsMonthlySeatMinimum < 0)
|
||||
{
|
||||
var teamsMinimumSeatsDisplayName = nameof(TeamsMonthlySeatMinimum).GetDisplayAttribute<CreateMspProviderModel>()?.GetName() ?? nameof(TeamsMonthlySeatMinimum);
|
||||
yield return new ValidationResult($"The {teamsMinimumSeatsDisplayName} field can not be negative.");
|
||||
}
|
||||
if (EnterpriseMonthlySeatMinimum < 0)
|
||||
{
|
||||
var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMonthlySeatMinimum).GetDisplayAttribute<CreateMspProviderModel>()?.GetName() ?? nameof(EnterpriseMonthlySeatMinimum);
|
||||
yield return new ValidationResult($"The {enterpriseMinimumSeatsDisplayName} field can not be negative.");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
public class CreateMultiOrganizationEnterpriseProviderModel : IValidatableObject
|
||||
{
|
||||
[Display(Name = "Owner Email")]
|
||||
public string OwnerEmail { get; set; }
|
||||
|
||||
[Display(Name = "Enterprise Seat Minimum")]
|
||||
public int EnterpriseSeatMinimum { get; set; }
|
||||
|
||||
[Display(Name = "Plan")]
|
||||
[Required]
|
||||
public PlanType? Plan { get; set; }
|
||||
|
||||
public virtual Provider ToProvider()
|
||||
{
|
||||
return new Provider
|
||||
{
|
||||
Type = ProviderType.MultiOrganizationEnterprise
|
||||
};
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(OwnerEmail))
|
||||
{
|
||||
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateMultiOrganizationEnterpriseProviderModel>()?.GetName() ?? nameof(OwnerEmail);
|
||||
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
|
||||
}
|
||||
if (EnterpriseSeatMinimum < 0)
|
||||
{
|
||||
var enterpriseSeatMinimumDisplayName = nameof(EnterpriseSeatMinimum).GetDisplayAttribute<CreateMultiOrganizationEnterpriseProviderModel>()?.GetName() ?? nameof(EnterpriseSeatMinimum);
|
||||
yield return new ValidationResult($"The {enterpriseSeatMinimumDisplayName} field can not be negative.");
|
||||
}
|
||||
if (Plan != PlanType.EnterpriseAnnually && Plan != PlanType.EnterpriseMonthly)
|
||||
{
|
||||
var planDisplayName = nameof(Plan).GetDisplayAttribute<CreateMultiOrganizationEnterpriseProviderModel>()?.GetName() ?? nameof(Plan);
|
||||
yield return new ValidationResult($"The {planDisplayName} field must be set to Enterprise Annually or Enterprise Monthly.");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,84 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
public class CreateProviderModel : IValidatableObject
|
||||
public class CreateProviderModel
|
||||
{
|
||||
public CreateProviderModel() { }
|
||||
|
||||
[Display(Name = "Provider Type")]
|
||||
public ProviderType Type { get; set; }
|
||||
|
||||
[Display(Name = "Owner Email")]
|
||||
public string OwnerEmail { get; set; }
|
||||
|
||||
[Display(Name = "Name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Display(Name = "Business Name")]
|
||||
public string BusinessName { get; set; }
|
||||
|
||||
[Display(Name = "Primary Billing Email")]
|
||||
public string BillingEmail { get; set; }
|
||||
|
||||
[Display(Name = "Teams (Monthly) Seat Minimum")]
|
||||
public int TeamsMonthlySeatMinimum { get; set; }
|
||||
|
||||
[Display(Name = "Enterprise (Monthly) Seat Minimum")]
|
||||
public int EnterpriseMonthlySeatMinimum { get; set; }
|
||||
|
||||
public virtual Provider ToProvider()
|
||||
{
|
||||
return new Provider()
|
||||
{
|
||||
Type = Type,
|
||||
Name = Name,
|
||||
BusinessName = BusinessName,
|
||||
BillingEmail = BillingEmail?.ToLowerInvariant().Trim()
|
||||
};
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case ProviderType.Msp:
|
||||
if (string.IsNullOrWhiteSpace(OwnerEmail))
|
||||
{
|
||||
var ownerEmailDisplayName = nameof(OwnerEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(OwnerEmail);
|
||||
yield return new ValidationResult($"The {ownerEmailDisplayName} field is required.");
|
||||
}
|
||||
if (TeamsMonthlySeatMinimum < 0)
|
||||
{
|
||||
var teamsMinimumSeatsDisplayName = nameof(TeamsMonthlySeatMinimum).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(TeamsMonthlySeatMinimum);
|
||||
yield return new ValidationResult($"The {teamsMinimumSeatsDisplayName} field can not be negative.");
|
||||
}
|
||||
if (EnterpriseMonthlySeatMinimum < 0)
|
||||
{
|
||||
var enterpriseMinimumSeatsDisplayName = nameof(EnterpriseMonthlySeatMinimum).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMonthlySeatMinimum);
|
||||
yield return new ValidationResult($"The {enterpriseMinimumSeatsDisplayName} field can not be negative.");
|
||||
}
|
||||
break;
|
||||
case ProviderType.Reseller:
|
||||
if (string.IsNullOrWhiteSpace(Name))
|
||||
{
|
||||
var nameDisplayName = nameof(Name).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(Name);
|
||||
yield return new ValidationResult($"The {nameDisplayName} field is required.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(BusinessName))
|
||||
{
|
||||
var businessNameDisplayName = nameof(BusinessName).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BusinessName);
|
||||
yield return new ValidationResult($"The {businessNameDisplayName} field is required.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(BillingEmail))
|
||||
{
|
||||
var billingEmailDisplayName = nameof(BillingEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BillingEmail);
|
||||
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
48
src/Admin/AdminConsole/Models/CreateResellerProviderModel.cs
Normal file
48
src/Admin/AdminConsole/Models/CreateResellerProviderModel.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
|
||||
namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
public class CreateResellerProviderModel : IValidatableObject
|
||||
{
|
||||
[Display(Name = "Name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[Display(Name = "Business Name")]
|
||||
public string BusinessName { get; set; }
|
||||
|
||||
[Display(Name = "Primary Billing Email")]
|
||||
public string BillingEmail { get; set; }
|
||||
|
||||
public virtual Provider ToProvider()
|
||||
{
|
||||
return new Provider
|
||||
{
|
||||
Name = Name,
|
||||
BusinessName = BusinessName,
|
||||
BillingEmail = BillingEmail?.ToLowerInvariant().Trim(),
|
||||
Type = ProviderType.Reseller
|
||||
};
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Name))
|
||||
{
|
||||
var nameDisplayName = nameof(Name).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(Name);
|
||||
yield return new ValidationResult($"The {nameDisplayName} field is required.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(BusinessName))
|
||||
{
|
||||
var businessNameDisplayName = nameof(BusinessName).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BusinessName);
|
||||
yield return new ValidationResult($"The {businessNameDisplayName} field is required.");
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(BillingEmail))
|
||||
{
|
||||
var billingEmailDisplayName = nameof(BillingEmail).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(BillingEmail);
|
||||
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,80 +1,48 @@
|
||||
@using Bit.SharedWeb.Utilities
|
||||
@using Bit.Core.AdminConsole.Enums.Provider
|
||||
@using Bit.Core
|
||||
|
||||
@model CreateProviderModel
|
||||
|
||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Create Provider";
|
||||
}
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
function toggleProviderTypeInfo(value) {
|
||||
document.querySelectorAll('[id^="info-"]').forEach(el => { el.classList.add('d-none'); });
|
||||
document.getElementById('info-' + value).classList.remove('d-none');
|
||||
}
|
||||
</script>
|
||||
var providerTypes = Enum.GetValues<ProviderType>()
|
||||
.OrderBy(x => x.GetDisplayAttribute().Order)
|
||||
.ToList();
|
||||
|
||||
if (!FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises))
|
||||
{
|
||||
providerTypes.Remove(ProviderType.MultiOrganizationEnterprise);
|
||||
}
|
||||
}
|
||||
|
||||
<h1>Create Provider</h1>
|
||||
|
||||
<form method="post">
|
||||
<form method="post" asp-action="Create">
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Type" class="h2"></label>
|
||||
@foreach(ProviderType providerType in Enum.GetValues(typeof(ProviderType)))
|
||||
@foreach (var providerType in providerTypes)
|
||||
{
|
||||
var providerTypeValue = (int)providerType;
|
||||
<div class="form-check">
|
||||
@Html.RadioButtonFor(m => m.Type, providerType, new { id = $"providerType-{providerTypeValue}", @class = "form-check-input", onclick=$"toggleProviderTypeInfo({providerTypeValue})" })
|
||||
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetName(), new { @class = "form-check-label align-middle", @for = $"providerType-{providerTypeValue}" })
|
||||
<br/>
|
||||
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetDescription(), new { @class = "form-check-label small text-muted ml-3 align-top", @for = $"providerType-{providerTypeValue}" })
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div id="@($"info-{(int)ProviderType.Msp}")" class="form-group @(Model.Type != ProviderType.Msp ? "d-none" : string.Empty)">
|
||||
<h2>MSP Info</h2>
|
||||
<div class="form-group">
|
||||
<label asp-for="OwnerEmail"></label>
|
||||
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||
</div>
|
||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
||||
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||
<div class="form-group">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
@Html.RadioButtonFor(m => m.Type, providerType, new { id = $"providerType-{providerTypeValue}", @class = "form-check-input" })
|
||||
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetName(), new { @class = "form-check-label align-middle", @for = $"providerType-{providerTypeValue}" })
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
||||
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
@Html.LabelFor(m => m.Type, providerType.GetDisplayAttribute()?.GetDescription(), new { @class = "form-check-label small text-muted align-top", @for = $"providerType-{providerTypeValue}" })
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div id="@($"info-{(int)ProviderType.Reseller}")" class="form-group @(Model.Type != ProviderType.Reseller ? "d-none" : string.Empty)">
|
||||
<h2>Reseller Info</h2>
|
||||
<div class="form-group">
|
||||
<label asp-for="Name"></label>
|
||||
<input type="text" class="form-control" asp-for="Name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="BusinessName"></label>
|
||||
<input type="text" class="form-control" asp-for="BusinessName">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="BillingEmail"></label>
|
||||
<input type="text" class="form-control" asp-for="BillingEmail">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||
<button type="submit" class="btn btn-primary mb-2">Next</button>
|
||||
</form>
|
||||
|
39
src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml
Normal file
39
src/Admin/AdminConsole/Views/Providers/CreateMsp.cshtml
Normal file
@ -0,0 +1,39 @@
|
||||
@using Bit.Core.AdminConsole.Enums.Provider
|
||||
@using Bit.Core
|
||||
|
||||
@model CreateMspProviderModel
|
||||
|
||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Create Managed Service Provider";
|
||||
}
|
||||
|
||||
<h1>Create Managed Service Provider</h1>
|
||||
<div>
|
||||
<form class="form-group" method="post" asp-action="CreateMsp">
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="OwnerEmail"></label>
|
||||
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||
</div>
|
||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling))
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
||||
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
||||
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||
</form>
|
||||
</div>
|
@ -0,0 +1,43 @@
|
||||
@using Bit.Core.Billing.Enums
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
|
||||
@model CreateMultiOrganizationEnterpriseProviderModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Create Multi-organization Enterprise Provider";
|
||||
}
|
||||
|
||||
<h1>Create Multi-organization Enterprise Provider</h1>
|
||||
<div>
|
||||
<form class="form-group" method="post" asp-action="CreateMultiOrganizationEnterprise">
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="OwnerEmail"></label>
|
||||
<input type="text" class="form-control" asp-for="OwnerEmail">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
@{
|
||||
var multiOrgPlans = new List<PlanType>
|
||||
{
|
||||
PlanType.EnterpriseAnnually,
|
||||
PlanType.EnterpriseMonthly
|
||||
};
|
||||
}
|
||||
<label asp-for="Plan"></label>
|
||||
<select class="form-control" asp-for="Plan" asp-items="Html.GetEnumSelectList(multiOrgPlans)">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="EnterpriseSeatMinimum"></label>
|
||||
<input type="number" class="form-control" asp-for="EnterpriseSeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||
</form>
|
||||
</div>
|
25
src/Admin/AdminConsole/Views/Providers/CreateReseller.cshtml
Normal file
25
src/Admin/AdminConsole/Views/Providers/CreateReseller.cshtml
Normal file
@ -0,0 +1,25 @@
|
||||
@model CreateResellerProviderModel
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Create Reseller Provider";
|
||||
}
|
||||
|
||||
<h1>Create Reseller Provider</h1>
|
||||
<div>
|
||||
<form class="form-group" method="post" asp-action="CreateReseller">
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Name"></label>
|
||||
<input type="text" class="form-control" asp-for="Name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="BusinessName"></label>
|
||||
<input type="text" class="form-control" asp-for="BusinessName">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="BillingEmail"></label>
|
||||
<input type="text" class="form-control" asp-for="BillingEmail">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mb-2">Create Provider</button>
|
||||
</form>
|
||||
</div>
|
19
src/Admin/Enums/HtmlHelperExtensions.cs
Normal file
19
src/Admin/Enums/HtmlHelperExtensions.cs
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
using Bit.SharedWeb.Utilities;
|
||||
|
||||
// ReSharper disable once CheckNamespace
|
||||
namespace Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
public static class HtmlHelper
|
||||
{
|
||||
public static IEnumerable<SelectListItem> GetEnumSelectList<T>(this IHtmlHelper htmlHelper, IEnumerable<T> values)
|
||||
where T : Enum
|
||||
{
|
||||
return values.Select(v => new SelectListItem
|
||||
{
|
||||
Text = v.GetDisplayAttribute().Name,
|
||||
Value = v.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -53,6 +53,8 @@ public class OrganizationUsersController : Controller
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand;
|
||||
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public OrganizationUsersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -73,7 +75,9 @@ public class OrganizationUsersController : Controller
|
||||
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand)
|
||||
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand,
|
||||
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -94,29 +98,34 @@ public class OrganizationUsersController : Controller
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand;
|
||||
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<OrganizationUserDetailsResponseModel> Get(string id, bool includeGroups = false)
|
||||
public async Task<OrganizationUserDetailsResponseModel> Get(Guid id, bool includeGroups = false)
|
||||
{
|
||||
var organizationUser = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(new Guid(id));
|
||||
if (organizationUser == null || !await _currentContext.ManageUsers(organizationUser.Item1.OrganizationId))
|
||||
var (organizationUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
|
||||
if (organizationUser == null || !await _currentContext.ManageUsers(organizationUser.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var response = new OrganizationUserDetailsResponseModel(organizationUser.Item1, organizationUser.Item2);
|
||||
var managedByOrganization = await GetManagedByOrganizationStatusAsync(
|
||||
organizationUser.OrganizationId,
|
||||
[organizationUser.Id]);
|
||||
|
||||
var response = new OrganizationUserDetailsResponseModel(organizationUser, managedByOrganization[organizationUser.Id], collections);
|
||||
|
||||
if (includeGroups)
|
||||
{
|
||||
response.Groups = await _groupRepository.GetManyIdsByUserIdAsync(organizationUser.Item1.Id);
|
||||
response.Groups = await _groupRepository.GetManyIdsByUserIdAsync(organizationUser.Id);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpGet("mini-details")]
|
||||
[RequireFeature(FeatureFlagKeys.Pm3478RefactorOrganizationUserApi)]
|
||||
public async Task<ListResponseModel<OrganizationUserUserMiniDetailsResponseModel>> GetMiniDetails(Guid orgId)
|
||||
{
|
||||
var authorizationResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId),
|
||||
@ -150,11 +159,13 @@ public class OrganizationUsersController : Controller
|
||||
}
|
||||
);
|
||||
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers);
|
||||
var organizationUsersManagementStatus = await GetManagedByOrganizationStatusAsync(orgId, organizationUsers.Select(o => o.Id));
|
||||
var responses = organizationUsers
|
||||
.Select(o =>
|
||||
{
|
||||
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == o.Id).twoFactorIsEnabled;
|
||||
var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled);
|
||||
var managedByOrganization = organizationUsersManagementStatus[o.Id];
|
||||
var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled, managedByOrganization);
|
||||
|
||||
return orgUser;
|
||||
});
|
||||
@ -682,4 +693,15 @@ public class OrganizationUsersController : Controller
|
||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r =>
|
||||
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
|
||||
}
|
||||
|
||||
private async Task<IDictionary<Guid, bool>> GetManagedByOrganizationStatusAsync(Guid orgId, IEnumerable<Guid> userIds)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
return userIds.ToDictionary(kvp => kvp, kvp => false);
|
||||
}
|
||||
|
||||
var usersOrganizationManagementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(orgId, userIds);
|
||||
return usersOrganizationManagementStatus;
|
||||
}
|
||||
}
|
||||
|
@ -64,20 +64,27 @@ public class OrganizationUserResponseModel : ResponseModel
|
||||
|
||||
public class OrganizationUserDetailsResponseModel : OrganizationUserResponseModel
|
||||
{
|
||||
public OrganizationUserDetailsResponseModel(OrganizationUser organizationUser,
|
||||
public OrganizationUserDetailsResponseModel(
|
||||
OrganizationUser organizationUser,
|
||||
bool managedByOrganization,
|
||||
IEnumerable<CollectionAccessSelection> collections)
|
||||
: base(organizationUser, "organizationUserDetails")
|
||||
{
|
||||
ManagedByOrganization = managedByOrganization;
|
||||
Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c));
|
||||
}
|
||||
|
||||
public OrganizationUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
|
||||
bool managedByOrganization,
|
||||
IEnumerable<CollectionAccessSelection> collections)
|
||||
: base(organizationUser, "organizationUserDetails")
|
||||
{
|
||||
ManagedByOrganization = managedByOrganization;
|
||||
Collections = collections.Select(c => new SelectionReadOnlyResponseModel(c));
|
||||
}
|
||||
|
||||
public bool ManagedByOrganization { get; set; }
|
||||
|
||||
public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
@ -110,7 +117,7 @@ public class OrganizationUserUserMiniDetailsResponseModel : ResponseModel
|
||||
public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel
|
||||
{
|
||||
public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser,
|
||||
bool twoFactorEnabled, string obj = "organizationUserUserDetails")
|
||||
bool twoFactorEnabled, bool managedByOrganization, string obj = "organizationUserUserDetails")
|
||||
: base(organizationUser, obj)
|
||||
{
|
||||
if (organizationUser == null)
|
||||
@ -127,6 +134,7 @@ public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponse
|
||||
Groups = organizationUser.Groups;
|
||||
// Prevent reset password when using key connector.
|
||||
ResetPasswordEnrolled = ResetPasswordEnrolled && !organizationUser.UsesKeyConnector;
|
||||
ManagedByOrganization = managedByOrganization;
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
@ -134,6 +142,11 @@ public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponse
|
||||
public string AvatarColor { get; set; }
|
||||
public bool TwoFactorEnabled { get; set; }
|
||||
public bool SsoBound { get; set; }
|
||||
/// <summary>
|
||||
/// Indicates if the organization manages the user. If a user is "managed" by an organization,
|
||||
/// the organization has greater control over their account, and some user actions are restricted.
|
||||
/// </summary>
|
||||
public bool ManagedByOrganization { get; set; }
|
||||
public IEnumerable<SelectionReadOnlyResponseModel> Collections { get; set; }
|
||||
public IEnumerable<Guid> Groups { get; set; }
|
||||
}
|
||||
|
@ -71,14 +71,13 @@ public class MembersController : Controller
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> Get(Guid id)
|
||||
{
|
||||
var userDetails = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
|
||||
var orgUser = userDetails?.Item1;
|
||||
var (orgUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id);
|
||||
if (orgUser == null || orgUser.OrganizationId != _currentContext.OrganizationId)
|
||||
{
|
||||
return new NotFoundResult();
|
||||
}
|
||||
var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser),
|
||||
userDetails.Item2);
|
||||
collections);
|
||||
return new JsonResult(response);
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,10 @@ namespace Bit.Core.AdminConsole.Enums.Provider;
|
||||
|
||||
public enum ProviderType : byte
|
||||
{
|
||||
[Display(ShortName = "MSP", Name = "Managed Service Provider", Description = "Access to clients organization")]
|
||||
[Display(ShortName = "MSP", Name = "Managed Service Provider", Description = "Access to clients organization", Order = 0)]
|
||||
Msp = 0,
|
||||
[Display(ShortName = "Reseller", Name = "Reseller", Description = "Access to clients billing")]
|
||||
[Display(ShortName = "Reseller", Name = "Reseller", Description = "Access to clients billing", Order = 1000)]
|
||||
Reseller = 1,
|
||||
[Display(ShortName = "MOE", Name = "Multi-organization Enterprise", Description = "Access to multiple organizations", Order = 1)]
|
||||
MultiOrganizationEnterprise = 2,
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
|
||||
public interface IOrganizationHasVerifiedDomainsQuery
|
||||
{
|
||||
Task<bool> HasVerifiedDomainsAsync(Guid orgId);
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
|
||||
public class OrganizationHasVerifiedDomainsQuery(IOrganizationDomainRepository domainRepository) : IOrganizationHasVerifiedDomainsQuery
|
||||
{
|
||||
public async Task<bool> HasVerifiedDomainsAsync(Guid orgId) =>
|
||||
(await domainRepository.GetDomainsByOrganizationIdAsync(orgId)).Any(od => od.VerifiedDate is not null);
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -15,6 +18,9 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
private readonly IDnsResolverService _dnsResolverService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
|
||||
|
||||
public VerifyOrganizationDomainCommand(
|
||||
@ -22,12 +28,18 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
IDnsResolverService dnsResolverService,
|
||||
IEventService eventService,
|
||||
IGlobalSettings globalSettings,
|
||||
IPolicyService policyService,
|
||||
IFeatureService featureService,
|
||||
IOrganizationService organizationService,
|
||||
ILogger<VerifyOrganizationDomainCommand> logger)
|
||||
{
|
||||
_organizationDomainRepository = organizationDomainRepository;
|
||||
_dnsResolverService = dnsResolverService;
|
||||
_eventService = eventService;
|
||||
_globalSettings = globalSettings;
|
||||
_policyService = policyService;
|
||||
_featureService = featureService;
|
||||
_organizationService = organizationService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -102,6 +114,8 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
if (await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt))
|
||||
{
|
||||
domain.SetVerifiedDate();
|
||||
|
||||
await EnableSingleOrganizationPolicyAsync(domain.OrganizationId);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -112,4 +126,13 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
|
||||
{
|
||||
await _policyService.SaveAsync(
|
||||
new Policy { OrganizationId = organizationId, Type = PolicyType.SingleOrg, Enabled = true }, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||
@ -10,12 +9,10 @@ public class OrganizationUserUserDetailsAuthorizationHandler
|
||||
: AuthorizationHandler<OrganizationUserUserDetailsOperationRequirement, OrganizationScope>
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public OrganizationUserUserDetailsAuthorizationHandler(ICurrentContext currentContext, IFeatureService featureService)
|
||||
public OrganizationUserUserDetailsAuthorizationHandler(ICurrentContext currentContext)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
@ -37,29 +34,6 @@ public class OrganizationUserUserDetailsAuthorizationHandler
|
||||
}
|
||||
|
||||
private async Task<bool> CanReadAllAsync(Guid organizationId)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.Pm3478RefactorOrganizationUserApi))
|
||||
{
|
||||
return await CanReadAllAsync_vNext(organizationId);
|
||||
}
|
||||
|
||||
return await CanReadAllAsync_vCurrent(organizationId);
|
||||
}
|
||||
|
||||
private async Task<bool> CanReadAllAsync_vCurrent(Guid organizationId)
|
||||
{
|
||||
// All users of an organization can read all other users of that organization for collection access management
|
||||
var org = _currentContext.GetOrganization(organizationId);
|
||||
if (org is not null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Allow provider users to read all organization users if they are a provider for the target organization
|
||||
return await _currentContext.ProviderUserForOrgAsync(organizationId);
|
||||
}
|
||||
|
||||
private async Task<bool> CanReadAllAsync_vNext(Guid organizationId)
|
||||
{
|
||||
// Admins can access this for general user management
|
||||
var organization = _currentContext.GetOrganization(organizationId);
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
|
||||
@ -6,4 +7,5 @@ public interface ICreateProviderCommand
|
||||
{
|
||||
Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats);
|
||||
Task CreateResellerAsync(Provider provider);
|
||||
Task CreateMultiOrganizationEnterpriseAsync(Provider provider, string ownerEmail, PlanType plan, int minimumSeats);
|
||||
}
|
||||
|
@ -22,8 +22,7 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
||||
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);
|
||||
Task<Tuple<OrganizationUser?, ICollection<CollectionAccessSelection>>> GetByIdWithCollectionsAsync(Guid id);
|
||||
Task<OrganizationUserUserDetails?> GetDetailsByIdAsync(Guid id);
|
||||
Task<Tuple<OrganizationUserUserDetails?, ICollection<CollectionAccessSelection>>>
|
||||
GetDetailsByIdWithCollectionsAsync(Guid id);
|
||||
Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithCollectionsAsync(Guid id);
|
||||
Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups = false, bool includeCollections = false);
|
||||
Task<ICollection<OrganizationUserOrganizationDetails>> GetManyDetailsByUserAsync(Guid userId,
|
||||
OrganizationUserStatusType? status = null);
|
||||
|
@ -4,8 +4,4 @@ public interface IOrganizationDomainService
|
||||
{
|
||||
Task ValidateOrganizationsDomainAsync();
|
||||
Task OrganizationDomainMaintenanceAsync();
|
||||
/// <summary>
|
||||
/// Indicates if the organization has any verified domains.
|
||||
/// </summary>
|
||||
Task<bool> HasVerifiedDomainsAsync(Guid orgId);
|
||||
}
|
||||
|
@ -106,12 +106,6 @@ public class OrganizationDomainService : IOrganizationDomainService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> HasVerifiedDomainsAsync(Guid orgId)
|
||||
{
|
||||
var orgDomains = await _domainRepository.GetDomainsByOrganizationIdAsync(orgId);
|
||||
return orgDomains.Any(od => od.VerifiedDate != null);
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetAdminEmailsAsync(Guid organizationId)
|
||||
{
|
||||
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
@ -32,6 +33,7 @@ public class PolicyService : IPolicyService
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISavePolicyCommand _savePolicyCommand;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
|
||||
|
||||
public PolicyService(
|
||||
IApplicationCacheService applicationCacheService,
|
||||
@ -45,7 +47,8 @@ public class PolicyService : IPolicyService
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IFeatureService featureService,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery)
|
||||
{
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_eventService = eventService;
|
||||
@ -59,6 +62,7 @@ public class PolicyService : IPolicyService
|
||||
_featureService = featureService;
|
||||
_savePolicyCommand = savePolicyCommand;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(Policy policy, Guid? savingUserId)
|
||||
@ -239,6 +243,7 @@ public class PolicyService : IPolicyService
|
||||
case PolicyType.SingleOrg:
|
||||
if (!policy.Enabled)
|
||||
{
|
||||
await HasVerifiedDomainsAsync(org);
|
||||
await RequiredBySsoAsync(org);
|
||||
await RequiredByVaultTimeoutAsync(org);
|
||||
await RequiredByKeyConnectorAsync(org);
|
||||
@ -279,6 +284,15 @@ public class PolicyService : IPolicyService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HasVerifiedDomainsAsync(Organization org)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(org.Id))
|
||||
{
|
||||
throw new BadRequestException("Organization has verified domains.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetPolicyConfiguration(Policy policy)
|
||||
{
|
||||
await _policyRepository.UpsertAsync(policy);
|
||||
|
@ -6,6 +6,14 @@ using Bit.Core.Utilities;
|
||||
namespace Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
public enum RegisterFinishTokenType : byte
|
||||
{
|
||||
EmailVerification = 1,
|
||||
OrganizationInvite = 2,
|
||||
OrgSponsoredFreeFamilyPlan = 3,
|
||||
EmergencyAccessInvite = 4,
|
||||
ProviderInvite = 5,
|
||||
}
|
||||
|
||||
public class RegisterFinishRequestModel : IValidatableObject
|
||||
{
|
||||
@ -36,6 +44,10 @@ public class RegisterFinishRequestModel : IValidatableObject
|
||||
public string? AcceptEmergencyAccessInviteToken { get; set; }
|
||||
public Guid? AcceptEmergencyAccessId { get; set; }
|
||||
|
||||
public string? ProviderInviteToken { get; set; }
|
||||
|
||||
public Guid? ProviderUserId { get; set; }
|
||||
|
||||
public User ToUser()
|
||||
{
|
||||
var user = new User
|
||||
@ -54,6 +66,32 @@ public class RegisterFinishRequestModel : IValidatableObject
|
||||
return user;
|
||||
}
|
||||
|
||||
public RegisterFinishTokenType GetTokenType()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(EmailVerificationToken))
|
||||
{
|
||||
return RegisterFinishTokenType.EmailVerification;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(OrgInviteToken) && OrganizationUserId.HasValue)
|
||||
{
|
||||
return RegisterFinishTokenType.OrganizationInvite;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(OrgSponsoredFreeFamilyPlanToken))
|
||||
{
|
||||
return RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(AcceptEmergencyAccessInviteToken) && AcceptEmergencyAccessId.HasValue)
|
||||
{
|
||||
return RegisterFinishTokenType.EmergencyAccessInvite;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(ProviderInviteToken) && ProviderUserId.HasValue)
|
||||
{
|
||||
return RegisterFinishTokenType.ProviderInvite;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Invalid token type.");
|
||||
}
|
||||
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
|
@ -61,4 +61,16 @@ public interface IRegisterUserCommand
|
||||
public Task<IdentityResult> RegisterUserViaAcceptEmergencyAccessInviteToken(User user, string masterPasswordHash,
|
||||
string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new user with a given master password hash, sends a welcome email, and raises the signup reference event.
|
||||
/// If a valid token is provided, the user will be created with their email verified.
|
||||
/// If the token is invalid or expired, an error will be thrown.
|
||||
/// </summary>
|
||||
/// <param name="user">The <see cref="User"/> to create</param>
|
||||
/// <param name="masterPasswordHash">The hashed master password the user entered</param>
|
||||
/// <param name="providerInviteToken">The provider invite token sent to the user via email</param>
|
||||
/// <param name="providerUserId">The provider user id which is used to validate the invite token</param>
|
||||
/// <returns><see cref="IdentityResult"/></returns>
|
||||
public Task<IdentityResult> RegisterUserViaProviderInviteToken(User user, string masterPasswordHash, string providerInviteToken, Guid providerUserId);
|
||||
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
|
||||
private readonly IDataProtector _organizationServiceDataProtector;
|
||||
private readonly IDataProtector _providerServiceDataProtector;
|
||||
|
||||
private readonly ICurrentContext _currentContext;
|
||||
|
||||
@ -75,6 +76,8 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
|
||||
_validateRedemptionTokenCommand = validateRedemptionTokenCommand;
|
||||
_emergencyAccessInviteTokenDataFactory = emergencyAccessInviteTokenDataFactory;
|
||||
|
||||
_providerServiceDataProtector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
||||
}
|
||||
|
||||
|
||||
@ -303,6 +306,25 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<IdentityResult> RegisterUserViaProviderInviteToken(User user, string masterPasswordHash,
|
||||
string providerInviteToken, Guid providerUserId)
|
||||
{
|
||||
ValidateOpenRegistrationAllowed();
|
||||
ValidateProviderInviteToken(providerInviteToken, providerUserId, user.Email);
|
||||
|
||||
user.EmailVerified = true;
|
||||
user.ApiKey = CoreHelpers.SecureRandomString(30); // API key can't be null.
|
||||
|
||||
var result = await _userService.CreateUserAsync(user, masterPasswordHash);
|
||||
if (result == IdentityResult.Success)
|
||||
{
|
||||
await _mailService.SendWelcomeEmailAsync(user);
|
||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ValidateOpenRegistrationAllowed()
|
||||
{
|
||||
// We validate open registration on send of initial email and here b/c a user could technically start the
|
||||
@ -333,6 +355,15 @@ public class RegisterUserCommand : IRegisterUserCommand
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateProviderInviteToken(string providerInviteToken, Guid providerUserId, string userEmail)
|
||||
{
|
||||
if (!CoreHelpers.TokenIsValid("ProviderUserInvite", _providerServiceDataProtector, providerInviteToken, userEmail, providerUserId,
|
||||
_globalSettings.OrganizationInviteExpirationHours))
|
||||
{
|
||||
throw new BadRequestException("Invalid provider invite token.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private RegistrationEmailVerificationTokenable ValidateRegistrationEmailVerificationTokenable(string emailVerificationToken, string userEmail)
|
||||
{
|
||||
|
@ -87,7 +87,9 @@ public record EnterprisePlan : Plan
|
||||
AdditionalStoragePricePerGb = 4;
|
||||
StripeStoragePlanId = "storage-gb-annually";
|
||||
StripeSeatPlanId = "2023-enterprise-org-seat-annually";
|
||||
StripeProviderPortalSeatPlanId = "password-manager-provider-portal-enterprise-annually-2024";
|
||||
SeatPrice = 72;
|
||||
ProviderPortalSeatPrice = 72;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -117,7 +117,6 @@ public static class FeatureFlagKeys
|
||||
public const string RestrictProviderAccess = "restrict-provider-access";
|
||||
public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service";
|
||||
public const string VaultBulkManagementAction = "vault-bulk-management-action";
|
||||
public const string BulkDeviceApproval = "bulk-device-approval";
|
||||
public const string MemberAccessReport = "ac-2059-member-access-report";
|
||||
public const string BlockLegacyUsers = "block-legacy-users";
|
||||
public const string InlineMenuFieldQualification = "inline-menu-field-qualification";
|
||||
@ -142,12 +141,13 @@ public static class FeatureFlagKeys
|
||||
public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill";
|
||||
public const string StorageReseedRefactor = "storage-reseed-refactor";
|
||||
public const string TrialPayment = "PM-8163-trial-payment";
|
||||
public const string Pm3478RefactorOrganizationUserApi = "pm-3478-refactor-organizationuser-api";
|
||||
public const string RemoveServerVersionHeader = "remove-server-version-header";
|
||||
public const string AccessIntelligence = "pm-13227-access-intelligence";
|
||||
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
||||
public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises";
|
||||
public const string Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions";
|
||||
public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split";
|
||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
@ -163,7 +163,6 @@ public static class FeatureFlagKeys
|
||||
return new Dictionary<string, string>()
|
||||
{
|
||||
{ DuoRedirect, "true" },
|
||||
{ BulkDeviceApproval, "true" },
|
||||
{ CipherKeyEncryption, "true" },
|
||||
};
|
||||
}
|
||||
|
@ -130,6 +130,7 @@ public static class OrganizationServiceCollectionExtensions
|
||||
services.AddScoped<IGetOrganizationDomainByIdOrganizationIdQuery, GetOrganizationDomainByIdOrganizationIdQuery>();
|
||||
services.AddScoped<IGetOrganizationDomainByOrganizationIdQuery, GetOrganizationDomainByOrganizationIdQuery>();
|
||||
services.AddScoped<IDeleteOrganizationDomainCommand, DeleteOrganizationDomainCommand>();
|
||||
services.AddScoped<IOrganizationHasVerifiedDomainsQuery, OrganizationHasVerifiedDomainsQuery>();
|
||||
}
|
||||
|
||||
private static void AddOrganizationAuthCommands(this IServiceCollection services)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core;
|
||||
using System.Diagnostics;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Auth.Models.Api.Response.Accounts;
|
||||
@ -149,40 +150,44 @@ public class AccountsController : Controller
|
||||
IdentityResult identityResult = null;
|
||||
var delaysEnabled = !_featureService.IsEnabled(FeatureFlagKeys.EmailVerificationDisableTimingDelays);
|
||||
|
||||
if (!string.IsNullOrEmpty(model.OrgInviteToken) && model.OrganizationUserId.HasValue)
|
||||
switch (model.GetTokenType())
|
||||
{
|
||||
identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash,
|
||||
model.OrgInviteToken, model.OrganizationUserId);
|
||||
case RegisterFinishTokenType.EmailVerification:
|
||||
identityResult =
|
||||
await _registerUserCommand.RegisterUserViaEmailVerificationToken(user, model.MasterPasswordHash,
|
||||
model.EmailVerificationToken);
|
||||
|
||||
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
|
||||
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
|
||||
break;
|
||||
case RegisterFinishTokenType.OrganizationInvite:
|
||||
identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash,
|
||||
model.OrgInviteToken, model.OrganizationUserId);
|
||||
|
||||
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
|
||||
break;
|
||||
case RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan:
|
||||
identityResult = await _registerUserCommand.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, model.MasterPasswordHash, model.OrgSponsoredFreeFamilyPlanToken);
|
||||
|
||||
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
|
||||
break;
|
||||
case RegisterFinishTokenType.EmergencyAccessInvite:
|
||||
Debug.Assert(model.AcceptEmergencyAccessId.HasValue);
|
||||
identityResult = await _registerUserCommand.RegisterUserViaAcceptEmergencyAccessInviteToken(user, model.MasterPasswordHash,
|
||||
model.AcceptEmergencyAccessInviteToken, model.AcceptEmergencyAccessId.Value);
|
||||
|
||||
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
|
||||
break;
|
||||
case RegisterFinishTokenType.ProviderInvite:
|
||||
Debug.Assert(model.ProviderUserId.HasValue);
|
||||
identityResult = await _registerUserCommand.RegisterUserViaProviderInviteToken(user, model.MasterPasswordHash,
|
||||
model.ProviderInviteToken, model.ProviderUserId.Value);
|
||||
|
||||
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new BadRequestException("Invalid registration finish request");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(model.OrgSponsoredFreeFamilyPlanToken))
|
||||
{
|
||||
identityResult = await _registerUserCommand.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, model.MasterPasswordHash, model.OrgSponsoredFreeFamilyPlanToken);
|
||||
|
||||
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(model.AcceptEmergencyAccessInviteToken) && model.AcceptEmergencyAccessId.HasValue)
|
||||
{
|
||||
identityResult = await _registerUserCommand.RegisterUserViaAcceptEmergencyAccessInviteToken(user, model.MasterPasswordHash,
|
||||
model.AcceptEmergencyAccessInviteToken, model.AcceptEmergencyAccessId.Value);
|
||||
|
||||
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(model.EmailVerificationToken))
|
||||
{
|
||||
throw new BadRequestException("Invalid registration finish request");
|
||||
}
|
||||
|
||||
identityResult =
|
||||
await _registerUserCommand.RegisterUserViaEmailVerificationToken(user, model.MasterPasswordHash,
|
||||
model.EmailVerificationToken);
|
||||
|
||||
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
|
||||
|
||||
}
|
||||
|
||||
private async Task<RegisterResponseModel> ProcessRegistrationResult(IdentityResult result, User user, bool delaysEnabled)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Identity.IdentityServer.RequestValidators;
|
||||
using Duende.IdentityServer.Models;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
@ -1,15 +1,10 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Api.Response;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@ -17,32 +12,26 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Api.Response;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||
|
||||
public abstract class BaseRequestValidator<T> where T : class
|
||||
{
|
||||
private UserManager<User> _userManager;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IDeviceValidator _deviceValidator;
|
||||
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
|
||||
private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly ILogger _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _tokenDataFactory;
|
||||
|
||||
protected ICurrentContext CurrentContext { get; }
|
||||
protected IPolicyService PolicyService { get; }
|
||||
@ -56,18 +45,14 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IDeviceValidator deviceValidator,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IMailService mailService,
|
||||
ILogger logger,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
IUserRepository userRepository,
|
||||
IPolicyService policyService,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||
@ -76,18 +61,14 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
_userService = userService;
|
||||
_eventService = eventService;
|
||||
_deviceValidator = deviceValidator;
|
||||
_organizationDuoWebTokenProvider = organizationDuoWebTokenProvider;
|
||||
_duoWebV4SDKService = duoWebV4SDKService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_mailService = mailService;
|
||||
_logger = logger;
|
||||
CurrentContext = currentContext;
|
||||
_globalSettings = globalSettings;
|
||||
PolicyService = policyService;
|
||||
_userRepository = userRepository;
|
||||
_tokenDataFactory = tokenDataFactory;
|
||||
FeatureService = featureService;
|
||||
SsoConfigRepository = ssoConfigRepository;
|
||||
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
|
||||
@ -104,12 +85,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
request.UserName, validatorContext.CaptchaResponse.Score);
|
||||
}
|
||||
|
||||
var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString();
|
||||
var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString();
|
||||
var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1";
|
||||
var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
|
||||
!string.IsNullOrWhiteSpace(twoFactorProvider);
|
||||
|
||||
var valid = await ValidateContextAsync(context, validatorContext);
|
||||
var user = validatorContext.User;
|
||||
if (!valid)
|
||||
@ -123,17 +98,37 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
return;
|
||||
}
|
||||
|
||||
var (isTwoFactorRequired, twoFactorOrganization) = await RequiresTwoFactorAsync(user, request);
|
||||
var (isTwoFactorRequired, twoFactorOrganization) = await _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(user, request);
|
||||
var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString();
|
||||
var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString();
|
||||
var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1";
|
||||
var validTwoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
|
||||
!string.IsNullOrWhiteSpace(twoFactorProvider);
|
||||
|
||||
if (isTwoFactorRequired)
|
||||
{
|
||||
if (!twoFactorRequest || !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType))
|
||||
// 2FA required and not provided response
|
||||
if (!validTwoFactorRequest ||
|
||||
!Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType))
|
||||
{
|
||||
await BuildTwoFactorResultAsync(user, twoFactorOrganization, context);
|
||||
var resultDict = await _twoFactorAuthenticationValidator
|
||||
.BuildTwoFactorResultAsync(user, twoFactorOrganization);
|
||||
if (resultDict == null)
|
||||
{
|
||||
await BuildErrorResultAsync("No two-step providers enabled.", false, context, user);
|
||||
return;
|
||||
}
|
||||
|
||||
// Include Master Password Policy in 2FA response
|
||||
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user));
|
||||
SetTwoFactorResult(context, resultDict);
|
||||
return;
|
||||
}
|
||||
|
||||
var verified = await VerifyTwoFactor(user, twoFactorOrganization,
|
||||
twoFactorProviderType, twoFactorToken);
|
||||
var verified = await _twoFactorAuthenticationValidator
|
||||
.VerifyTwoFactor(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken);
|
||||
|
||||
// 2FA required but request not valid or remember token expired response
|
||||
if (!verified || isBot)
|
||||
{
|
||||
if (twoFactorProviderType != TwoFactorProviderType.Remember)
|
||||
@ -143,16 +138,20 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
}
|
||||
else if (twoFactorProviderType == TwoFactorProviderType.Remember)
|
||||
{
|
||||
await BuildTwoFactorResultAsync(user, twoFactorOrganization, context);
|
||||
var resultDict = await _twoFactorAuthenticationValidator
|
||||
.BuildTwoFactorResultAsync(user, twoFactorOrganization);
|
||||
|
||||
// Include Master Password Policy in 2FA response
|
||||
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user));
|
||||
SetTwoFactorResult(context, resultDict);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
twoFactorRequest = false;
|
||||
validTwoFactorRequest = false;
|
||||
twoFactorRemember = false;
|
||||
twoFactorToken = null;
|
||||
}
|
||||
|
||||
// Force legacy users to the web for migration
|
||||
@ -165,7 +164,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if can finish validation process
|
||||
if (await IsValidAuthTypeAsync(user, request.GrantType))
|
||||
{
|
||||
var device = await _deviceValidator.SaveDeviceAsync(user, request);
|
||||
@ -174,8 +172,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
await BuildErrorResultAsync("No device information provided.", false, context, user);
|
||||
return;
|
||||
}
|
||||
|
||||
await BuildSuccessResultAsync(user, context, device, twoFactorRequest && twoFactorRemember);
|
||||
await BuildSuccessResultAsync(user, context, device, validTwoFactorRequest && twoFactorRemember);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -238,67 +235,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
await SetSuccessResult(context, user, claims, customResponse);
|
||||
}
|
||||
|
||||
protected async Task BuildTwoFactorResultAsync(User user, Organization organization, T context)
|
||||
{
|
||||
var providerKeys = new List<byte>();
|
||||
var providers = new Dictionary<string, Dictionary<string, object>>();
|
||||
|
||||
var enabledProviders = new List<KeyValuePair<TwoFactorProviderType, TwoFactorProvider>>();
|
||||
if (organization?.GetTwoFactorProviders() != null)
|
||||
{
|
||||
enabledProviders.AddRange(organization.GetTwoFactorProviders().Where(
|
||||
p => organization.TwoFactorProviderIsEnabled(p.Key)));
|
||||
}
|
||||
|
||||
if (user.GetTwoFactorProviders() != null)
|
||||
{
|
||||
foreach (var p in user.GetTwoFactorProviders())
|
||||
{
|
||||
if (await _userService.TwoFactorProviderIsEnabledAsync(p.Key, user))
|
||||
{
|
||||
enabledProviders.Add(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!enabledProviders.Any())
|
||||
{
|
||||
await BuildErrorResultAsync("No two-step providers enabled.", false, context, user);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var provider in enabledProviders)
|
||||
{
|
||||
providerKeys.Add((byte)provider.Key);
|
||||
var infoDict = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value);
|
||||
providers.Add(((byte)provider.Key).ToString(), infoDict);
|
||||
}
|
||||
|
||||
var twoFactorResultDict = new Dictionary<string, object>
|
||||
{
|
||||
{ "TwoFactorProviders", providers.Keys },
|
||||
{ "TwoFactorProviders2", providers },
|
||||
{ "MasterPasswordPolicy", await GetMasterPasswordPolicy(user) },
|
||||
};
|
||||
|
||||
// If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token
|
||||
if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email))
|
||||
{
|
||||
twoFactorResultDict.Add("SsoEmail2faSessionToken",
|
||||
_tokenDataFactory.Protect(new SsoEmail2faSessionTokenable(user)));
|
||||
|
||||
twoFactorResultDict.Add("Email", user.Email);
|
||||
}
|
||||
|
||||
SetTwoFactorResult(context, twoFactorResultDict);
|
||||
|
||||
if (enabledProviders.Count() == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email)
|
||||
{
|
||||
// Send email now if this is their only 2FA method
|
||||
await _userService.SendTwoFactorEmailAsync(user);
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task BuildErrorResultAsync(string message, bool twoFactorRequest, T context, User user)
|
||||
{
|
||||
if (user != null)
|
||||
@ -329,35 +265,13 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
protected abstract void SetErrorResult(T context, Dictionary<string, object> customResponse);
|
||||
protected abstract ClaimsPrincipal GetSubject(T context);
|
||||
|
||||
protected virtual async Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)
|
||||
{
|
||||
if (request.GrantType == "client_credentials")
|
||||
{
|
||||
// Do not require MFA for api key logins
|
||||
return new Tuple<bool, Organization>(false, null);
|
||||
}
|
||||
|
||||
var individualRequired = _userManager.SupportsUserTwoFactor &&
|
||||
await _userManager.GetTwoFactorEnabledAsync(user) &&
|
||||
(await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0;
|
||||
|
||||
Organization firstEnabledOrg = null;
|
||||
var orgs = (await CurrentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)).ToList();
|
||||
if (orgs.Count > 0)
|
||||
{
|
||||
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
var twoFactorOrgs = orgs.Where(o => OrgUsing2fa(orgAbilities, o.Id));
|
||||
if (twoFactorOrgs.Any())
|
||||
{
|
||||
var userOrgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);
|
||||
firstEnabledOrg = userOrgs.FirstOrDefault(
|
||||
o => orgs.Any(om => om.Id == o.Id) && o.TwoFactorIsEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
return new Tuple<bool, Organization>(individualRequired || firstEnabledOrg != null, firstEnabledOrg);
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
/// <param name="user">user trying to login</param>
|
||||
/// <param name="grantType">magic string identifying the grant type requested</param>
|
||||
/// <returns></returns>
|
||||
private async Task<bool> IsValidAuthTypeAsync(User user, string grantType)
|
||||
{
|
||||
if (grantType == "authorization_code" || grantType == "client_credentials")
|
||||
@ -367,7 +281,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check if user belongs to any organization with an active SSO policy
|
||||
var anySsoPoliciesApplicableToUser = await PolicyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
||||
if (anySsoPoliciesApplicableToUser)
|
||||
@ -379,134 +292,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool OrgUsing2fa(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
|
||||
{
|
||||
return orgAbilities != null && orgAbilities.ContainsKey(orgId) &&
|
||||
orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa;
|
||||
}
|
||||
|
||||
private async Task<bool> VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type,
|
||||
string token)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TwoFactorProviderType.Authenticator:
|
||||
case TwoFactorProviderType.Email:
|
||||
case TwoFactorProviderType.Duo:
|
||||
case TwoFactorProviderType.YubiKey:
|
||||
case TwoFactorProviderType.WebAuthn:
|
||||
case TwoFactorProviderType.Remember:
|
||||
if (type != TwoFactorProviderType.Remember &&
|
||||
!await _userService.TwoFactorProviderIsEnabledAsync(type, user))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt
|
||||
if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect))
|
||||
{
|
||||
if (type == TwoFactorProviderType.Duo)
|
||||
{
|
||||
if (!token.Contains(':'))
|
||||
{
|
||||
// We have to send the provider to the DuoWebV4SDKService to create the DuoClient
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
|
||||
return await _duoWebV4SDKService.ValidateAsync(token, provider, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await _userManager.VerifyTwoFactorTokenAsync(user,
|
||||
CoreHelpers.CustomProviderName(type), token);
|
||||
case TwoFactorProviderType.OrganizationDuo:
|
||||
if (!organization?.TwoFactorProviderIsEnabled(type) ?? true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt
|
||||
if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect))
|
||||
{
|
||||
if (type == TwoFactorProviderType.OrganizationDuo)
|
||||
{
|
||||
if (!token.Contains(':'))
|
||||
{
|
||||
// We have to send the provider to the DuoWebV4SDKService to create the DuoClient
|
||||
var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
|
||||
return await _duoWebV4SDKService.ValidateAsync(token, provider, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, object>> BuildTwoFactorParams(Organization organization, User user,
|
||||
TwoFactorProviderType type, TwoFactorProvider provider)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case TwoFactorProviderType.Duo:
|
||||
case TwoFactorProviderType.WebAuthn:
|
||||
case TwoFactorProviderType.Email:
|
||||
case TwoFactorProviderType.YubiKey:
|
||||
if (!await _userService.TwoFactorProviderIsEnabledAsync(type, user))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
|
||||
CoreHelpers.CustomProviderName(type));
|
||||
if (type == TwoFactorProviderType.Duo)
|
||||
{
|
||||
var duoResponse = new Dictionary<string, object>
|
||||
{
|
||||
["Host"] = provider.MetaData["Host"],
|
||||
["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, user),
|
||||
};
|
||||
|
||||
return duoResponse;
|
||||
}
|
||||
else if (type == TwoFactorProviderType.WebAuthn)
|
||||
{
|
||||
if (token == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<Dictionary<string, object>>(token);
|
||||
}
|
||||
else if (type == TwoFactorProviderType.Email)
|
||||
{
|
||||
var twoFactorEmail = (string)provider.MetaData["Email"];
|
||||
var redactedEmail = CoreHelpers.RedactEmailAddress(twoFactorEmail);
|
||||
return new Dictionary<string, object> { ["Email"] = redactedEmail };
|
||||
}
|
||||
else if (type == TwoFactorProviderType.YubiKey)
|
||||
{
|
||||
return new Dictionary<string, object> { ["Nfc"] = (bool)provider.MetaData["Nfc"] };
|
||||
}
|
||||
|
||||
return null;
|
||||
case TwoFactorProviderType.OrganizationDuo:
|
||||
if (await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization))
|
||||
{
|
||||
var duoResponse = new Dictionary<string, object>
|
||||
{
|
||||
["Host"] = provider.MetaData["Host"],
|
||||
["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, user),
|
||||
};
|
||||
|
||||
return duoResponse;
|
||||
}
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ResetFailedAuthDetailsAsync(User user)
|
||||
{
|
||||
// Early escape if db hit not necessary
|
||||
@ -546,7 +331,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// checks to see if a user is trying to log into a new device
|
||||
/// checks to see if a user is trying to log into a new device
|
||||
/// and has reached the maximum number of failed login attempts.
|
||||
/// </summary>
|
||||
/// <param name="unknownDevice">boolean</param>
|
@ -1,9 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Models.Api.Response;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@ -11,7 +9,6 @@ using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using HandlebarsDotNet;
|
||||
@ -20,7 +17,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||
|
||||
public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenRequestValidationContext>,
|
||||
ICustomTokenRequestValidator
|
||||
@ -29,28 +26,36 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
|
||||
public CustomTokenRequestValidator(
|
||||
UserManager<User> userManager,
|
||||
IDeviceValidator deviceValidator,
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IDeviceValidator deviceValidator,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IMailService mailService,
|
||||
ILogger<CustomTokenRequestValidator> logger,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserRepository userRepository,
|
||||
IPolicyService policyService,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||
: base(userManager, userService, eventService, deviceValidator,
|
||||
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
|
||||
applicationCacheService, mailService, logger, currentContext, globalSettings,
|
||||
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder
|
||||
)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
eventService,
|
||||
deviceValidator,
|
||||
twoFactorAuthenticationValidator,
|
||||
organizationUserRepository,
|
||||
mailService,
|
||||
logger,
|
||||
currentContext,
|
||||
globalSettings,
|
||||
userRepository,
|
||||
policyService,
|
||||
featureService,
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder)
|
||||
{
|
||||
_userManager = userManager;
|
||||
@ -70,7 +75,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
}
|
||||
}
|
||||
|
||||
string[] allowedGrantTypes = { "authorization_code", "client_credentials" };
|
||||
string[] allowedGrantTypes = ["authorization_code", "client_credentials"];
|
||||
if (!allowedGrantTypes.Contains(context.Result.ValidatedRequest.GrantType)
|
||||
|| context.Result.ValidatedRequest.ClientId.StartsWith("organization")
|
||||
|| context.Result.ValidatedRequest.ClientId.StartsWith("installation")
|
@ -8,7 +8,7 @@ using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Duende.IdentityServer.Validation;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||
|
||||
public interface IDeviceValidator
|
||||
{
|
||||
@ -41,6 +41,12 @@ public class DeviceValidator(
|
||||
private readonly IMailService _mailService = mailService;
|
||||
private readonly ICurrentContext _currentContext = currentContext;
|
||||
|
||||
/// <summary>
|
||||
/// Save a device to the database. If the device is already known, it will be returned.
|
||||
/// </summary>
|
||||
/// <param name="user">The user is assumed NOT null, still going to check though</param>
|
||||
/// <param name="request">Duende Validated Request that contains the data to create the device object</param>
|
||||
/// <returns>Returns null if user or device is malformed; The existing device if already in DB; a new device login</returns>
|
||||
public async Task<Device> SaveDeviceAsync(User user, ValidatedTokenRequest request)
|
||||
{
|
||||
var device = GetDeviceFromRequest(request);
|
@ -1,8 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Context;
|
||||
@ -10,13 +8,12 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||
|
||||
public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwnerPasswordValidationContext>,
|
||||
IResourceOwnerPasswordValidator
|
||||
@ -31,11 +28,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IDeviceValidator deviceValidator,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IMailService mailService,
|
||||
ILogger<ResourceOwnerPasswordValidator> logger,
|
||||
ICurrentContext currentContext,
|
||||
@ -44,14 +38,25 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IUserRepository userRepository,
|
||||
IPolicyService policyService,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||
: base(userManager, userService, eventService, deviceValidator,
|
||||
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
|
||||
applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService,
|
||||
tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
eventService,
|
||||
deviceValidator,
|
||||
twoFactorAuthenticationValidator,
|
||||
organizationUserRepository,
|
||||
mailService,
|
||||
logger,
|
||||
currentContext,
|
||||
globalSettings,
|
||||
userRepository,
|
||||
policyService,
|
||||
featureService,
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_currentContext = currentContext;
|
@ -0,0 +1,297 @@
|
||||
|
||||
using System.Text.Json;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||
|
||||
public interface ITwoFactorAuthenticationValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Check if the user is required to use two-factor authentication to login. This is based on the user's
|
||||
/// enabled two-factor providers, the user's organizations enabled two-factor providers, and the grant type.
|
||||
/// Client credentials and webauthn grant types do not require two-factor authentication.
|
||||
/// </summary>
|
||||
/// <param name="user">the active user for the request</param>
|
||||
/// <param name="request">the request that contains the grant types</param>
|
||||
/// <returns>boolean</returns>
|
||||
Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request);
|
||||
/// <summary>
|
||||
/// Builds the two-factor authentication result for the user based on the available two-factor providers
|
||||
/// from either their user account or Organization.
|
||||
/// </summary>
|
||||
/// <param name="user">user trying to login</param>
|
||||
/// <param name="organization">organization associated with the user; Can be null</param>
|
||||
/// <returns>Dictionary with the TwoFactorProviderType as the Key and the Provider Metadata as the Value</returns>
|
||||
Task<Dictionary<string, object>> BuildTwoFactorResultAsync(User user, Organization organization);
|
||||
/// <summary>
|
||||
/// Uses the built in userManager methods to verify the two-factor token for the user. If the organization uses
|
||||
/// organization duo, it will use the organization duo token provider to verify the token.
|
||||
/// </summary>
|
||||
/// <param name="user">the active User</param>
|
||||
/// <param name="organization">organization of user; can be null</param>
|
||||
/// <param name="twoFactorProviderType">Two Factor Provider to use to verify the token</param>
|
||||
/// <param name="token">secret passed from the user and consumed by the two-factor provider's verify method</param>
|
||||
/// <returns>boolean</returns>
|
||||
Task<bool> VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType twoFactorProviderType, string token);
|
||||
}
|
||||
|
||||
public class TwoFactorAuthenticationValidator(
|
||||
IUserService userService,
|
||||
UserManager<User> userManager,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
||||
IFeatureService featureService,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmail2faSessionTokeFactory,
|
||||
ICurrentContext currentContext) : ITwoFactorAuthenticationValidator
|
||||
{
|
||||
private readonly IUserService _userService = userService;
|
||||
private readonly UserManager<User> _userManager = userManager;
|
||||
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider = organizationDuoWebTokenProvider;
|
||||
private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService = duoWebV4SDKService;
|
||||
private readonly IFeatureService _featureService = featureService;
|
||||
private readonly IApplicationCacheService _applicationCacheService = applicationCacheService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository = organizationRepository;
|
||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmail2faSessionTokeFactory = ssoEmail2faSessionTokeFactory;
|
||||
private readonly ICurrentContext _currentContext = currentContext;
|
||||
|
||||
public async Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)
|
||||
{
|
||||
if (request.GrantType == "client_credentials" || request.GrantType == "webauthn")
|
||||
{
|
||||
/*
|
||||
Do not require MFA for api key logins.
|
||||
We consider Fido2 userVerification a second factor, so we don't require a second factor here.
|
||||
*/
|
||||
return new Tuple<bool, Organization>(false, null);
|
||||
}
|
||||
|
||||
var individualRequired = _userManager.SupportsUserTwoFactor &&
|
||||
await _userManager.GetTwoFactorEnabledAsync(user) &&
|
||||
(await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0;
|
||||
|
||||
Organization firstEnabledOrg = null;
|
||||
var orgs = (await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)).ToList();
|
||||
if (orgs.Count > 0)
|
||||
{
|
||||
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
var twoFactorOrgs = orgs.Where(o => OrgUsing2fa(orgAbilities, o.Id));
|
||||
if (twoFactorOrgs.Any())
|
||||
{
|
||||
var userOrgs = await _organizationRepository.GetManyByUserIdAsync(user.Id);
|
||||
firstEnabledOrg = userOrgs.FirstOrDefault(
|
||||
o => orgs.Any(om => om.Id == o.Id) && o.TwoFactorIsEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
return new Tuple<bool, Organization>(individualRequired || firstEnabledOrg != null, firstEnabledOrg);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, object>> BuildTwoFactorResultAsync(User user, Organization organization)
|
||||
{
|
||||
var enabledProviders = await GetEnabledTwoFactorProvidersAsync(user, organization);
|
||||
if (enabledProviders.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var providers = new Dictionary<string, Dictionary<string, object>>();
|
||||
foreach (var provider in enabledProviders)
|
||||
{
|
||||
var twoFactorParams = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value);
|
||||
providers.Add(((byte)provider.Key).ToString(), twoFactorParams);
|
||||
}
|
||||
|
||||
var twoFactorResultDict = new Dictionary<string, object>
|
||||
{
|
||||
{ "TwoFactorProviders", null },
|
||||
{ "TwoFactorProviders2", providers }, // backwards compatibility
|
||||
};
|
||||
|
||||
// If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token
|
||||
if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email))
|
||||
{
|
||||
twoFactorResultDict.Add("SsoEmail2faSessionToken",
|
||||
_ssoEmail2faSessionTokeFactory.Protect(new SsoEmail2faSessionTokenable(user)));
|
||||
|
||||
twoFactorResultDict.Add("Email", user.Email);
|
||||
}
|
||||
|
||||
if (enabledProviders.Count == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email)
|
||||
{
|
||||
// Send email now if this is their only 2FA method
|
||||
await _userService.SendTwoFactorEmailAsync(user);
|
||||
}
|
||||
|
||||
return twoFactorResultDict;
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyTwoFactor(
|
||||
User user,
|
||||
Organization organization,
|
||||
TwoFactorProviderType type,
|
||||
string token)
|
||||
{
|
||||
if (organization != null && type == TwoFactorProviderType.OrganizationDuo)
|
||||
{
|
||||
if (organization.TwoFactorProviderIsEnabled(type))
|
||||
{
|
||||
// DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect))
|
||||
{
|
||||
if (!token.Contains(':'))
|
||||
{
|
||||
// We have to send the provider to the DuoWebV4SDKService to create the DuoClient
|
||||
var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo);
|
||||
return await _duoWebV4SDKService.ValidateAsync(token, provider, user);
|
||||
}
|
||||
}
|
||||
return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case TwoFactorProviderType.Authenticator:
|
||||
case TwoFactorProviderType.Email:
|
||||
case TwoFactorProviderType.Duo:
|
||||
case TwoFactorProviderType.YubiKey:
|
||||
case TwoFactorProviderType.WebAuthn:
|
||||
case TwoFactorProviderType.Remember:
|
||||
if (type != TwoFactorProviderType.Remember &&
|
||||
!await _userService.TwoFactorProviderIsEnabledAsync(type, user))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect))
|
||||
{
|
||||
if (type == TwoFactorProviderType.Duo)
|
||||
{
|
||||
if (!token.Contains(':'))
|
||||
{
|
||||
// We have to send the provider to the DuoWebV4SDKService to create the DuoClient
|
||||
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo);
|
||||
return await _duoWebV4SDKService.ValidateAsync(token, provider, user);
|
||||
}
|
||||
}
|
||||
}
|
||||
return await _userManager.VerifyTwoFactorTokenAsync(user,
|
||||
CoreHelpers.CustomProviderName(type), token);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<KeyValuePair<TwoFactorProviderType, TwoFactorProvider>>> GetEnabledTwoFactorProvidersAsync(
|
||||
User user, Organization organization)
|
||||
{
|
||||
var enabledProviders = new List<KeyValuePair<TwoFactorProviderType, TwoFactorProvider>>();
|
||||
var organizationTwoFactorProviders = organization?.GetTwoFactorProviders();
|
||||
if (organizationTwoFactorProviders != null)
|
||||
{
|
||||
enabledProviders.AddRange(
|
||||
organizationTwoFactorProviders.Where(
|
||||
p => (p.Value?.Enabled ?? false) && organization.Use2fa));
|
||||
}
|
||||
|
||||
var userTwoFactorProviders = user.GetTwoFactorProviders();
|
||||
var userCanAccessPremium = await _userService.CanAccessPremium(user);
|
||||
if (userTwoFactorProviders != null)
|
||||
{
|
||||
enabledProviders.AddRange(
|
||||
userTwoFactorProviders.Where(p =>
|
||||
// Providers that do not require premium
|
||||
(p.Value.Enabled && !TwoFactorProvider.RequiresPremium(p.Key)) ||
|
||||
// Providers that require premium and the User has Premium
|
||||
(p.Value.Enabled && TwoFactorProvider.RequiresPremium(p.Key) && userCanAccessPremium)));
|
||||
}
|
||||
|
||||
return enabledProviders;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the parameters for the two-factor authentication
|
||||
/// </summary>
|
||||
/// <param name="organization">We need the organization for Organization Duo Provider type</param>
|
||||
/// <param name="user">The user for which the token is being generated</param>
|
||||
/// <param name="type">Provider Type</param>
|
||||
/// <param name="provider">Raw data that is used to create the response</param>
|
||||
/// <returns>a dictionary with the correct provider configuration or null if the provider is not configured properly</returns>
|
||||
private async Task<Dictionary<string, object>> BuildTwoFactorParams(Organization organization, User user,
|
||||
TwoFactorProviderType type, TwoFactorProvider provider)
|
||||
{
|
||||
// We will always return this dictionary. If none of the criteria is met then it will return null.
|
||||
var twoFactorParams = new Dictionary<string, object>();
|
||||
|
||||
// OrganizationDuo is odd since it doesn't use the UserManager built-in TwoFactor flows
|
||||
/*
|
||||
Note: Duo is in the midst of being updated to use the UserManager built-in TwoFactor class
|
||||
in the future the `AuthUrl` will be the generated "token" - PM-8107
|
||||
*/
|
||||
if (type == TwoFactorProviderType.OrganizationDuo &&
|
||||
await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization))
|
||||
{
|
||||
twoFactorParams.Add("Host", provider.MetaData["Host"]);
|
||||
twoFactorParams.Add("AuthUrl", await _duoWebV4SDKService.GenerateAsync(provider, user));
|
||||
|
||||
return twoFactorParams;
|
||||
}
|
||||
|
||||
// Individual 2FA providers use the UserManager built-in TwoFactor flow so we can generate the token before building the params
|
||||
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
|
||||
CoreHelpers.CustomProviderName(type));
|
||||
switch (type)
|
||||
{
|
||||
/*
|
||||
Note: Duo is in the midst of being updated to use the UserManager built-in TwoFactor class
|
||||
in the future the `AuthUrl` will be the generated "token" - PM-8107
|
||||
*/
|
||||
case TwoFactorProviderType.Duo:
|
||||
twoFactorParams.Add("Host", provider.MetaData["Host"]);
|
||||
twoFactorParams.Add("AuthUrl", await _duoWebV4SDKService.GenerateAsync(provider, user));
|
||||
break;
|
||||
case TwoFactorProviderType.WebAuthn:
|
||||
if (token != null)
|
||||
{
|
||||
twoFactorParams = JsonSerializer.Deserialize<Dictionary<string, object>>(token);
|
||||
}
|
||||
break;
|
||||
case TwoFactorProviderType.Email:
|
||||
var twoFactorEmail = (string)provider.MetaData["Email"];
|
||||
var redactedEmail = CoreHelpers.RedactEmailAddress(twoFactorEmail);
|
||||
twoFactorParams.Add("Email", redactedEmail);
|
||||
break;
|
||||
case TwoFactorProviderType.YubiKey:
|
||||
twoFactorParams.Add("Nfc", (bool)provider.MetaData["Nfc"]);
|
||||
break;
|
||||
}
|
||||
|
||||
// return null if the dictionary is empty
|
||||
return twoFactorParams.Count > 0 ? twoFactorParams : null;
|
||||
}
|
||||
|
||||
private bool OrgUsing2fa(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
|
||||
{
|
||||
return orgAbilities != null && orgAbilities.ContainsKey(orgId) &&
|
||||
orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa;
|
||||
}
|
||||
}
|
@ -1,10 +1,8 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||
@ -19,7 +17,7 @@ using Duende.IdentityServer.Validation;
|
||||
using Fido2NetLib;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
namespace Bit.Identity.IdentityServer.RequestValidators;
|
||||
|
||||
public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidationContext>, IExtensionGrantValidator
|
||||
{
|
||||
@ -34,11 +32,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IDeviceValidator deviceValidator,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IMailService mailService,
|
||||
ILogger<CustomTokenRequestValidator> logger,
|
||||
ICurrentContext currentContext,
|
||||
@ -46,16 +41,27 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserRepository userRepository,
|
||||
IPolicyService policyService,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
||||
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
|
||||
IFeatureService featureService,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand
|
||||
)
|
||||
: base(userManager, userService, eventService, deviceValidator,
|
||||
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
|
||||
applicationCacheService, mailService, logger, currentContext, globalSettings,
|
||||
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
eventService,
|
||||
deviceValidator,
|
||||
twoFactorAuthenticationValidator,
|
||||
organizationUserRepository,
|
||||
mailService,
|
||||
logger,
|
||||
currentContext,
|
||||
globalSettings,
|
||||
userRepository,
|
||||
policyService,
|
||||
featureService,
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder)
|
||||
{
|
||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
||||
@ -122,12 +128,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
return context.Result.Subject;
|
||||
}
|
||||
|
||||
protected override Task<Tuple<bool, Organization>> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request)
|
||||
{
|
||||
// We consider Fido2 userVerification a second factor, so we don't require a second factor here.
|
||||
return Task.FromResult(new Tuple<bool, Organization>(false, null));
|
||||
}
|
||||
|
||||
protected override void SetTwoFactorResult(ExtensionGrantValidationContext context,
|
||||
Dictionary<string, object> customResponse)
|
||||
{
|
@ -3,6 +3,7 @@ using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Bit.Identity.IdentityServer.RequestValidators;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Duende.IdentityServer.ResponseHandling;
|
||||
using Duende.IdentityServer.Services;
|
||||
@ -21,6 +22,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddTransient<IAuthorizationCodeStore, AuthorizationCodeStore>();
|
||||
services.AddTransient<IUserDecryptionOptionsBuilder, UserDecryptionOptionsBuilder>();
|
||||
services.AddTransient<IDeviceValidator, DeviceValidator>();
|
||||
services.AddTransient<ITwoFactorAuthenticationValidator, TwoFactorAuthenticationValidator>();
|
||||
|
||||
var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity);
|
||||
var identityServerBuilder = services
|
||||
|
@ -196,8 +196,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
return results.SingleOrDefault();
|
||||
}
|
||||
}
|
||||
public async Task<Tuple<OrganizationUserUserDetails?, ICollection<CollectionAccessSelection>>>
|
||||
GetDetailsByIdWithCollectionsAsync(Guid id)
|
||||
public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithCollectionsAsync(Guid id)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
@ -206,9 +205,9 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
new { Id = id },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
var user = (await results.ReadAsync<OrganizationUserUserDetails>()).SingleOrDefault();
|
||||
var organizationUserUserDetails = (await results.ReadAsync<OrganizationUserUserDetails>()).SingleOrDefault();
|
||||
var collections = (await results.ReadAsync<CollectionAccessSelection>()).ToList();
|
||||
return new Tuple<OrganizationUserUserDetails?, ICollection<CollectionAccessSelection>>(user, collections);
|
||||
return (organizationUserUserDetails, collections);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,7 +248,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Tuple<OrganizationUserUserDetails, ICollection<CollectionAccessSelection>>> GetDetailsByIdWithCollectionsAsync(Guid id)
|
||||
public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection<CollectionAccessSelection> Collections)> GetDetailsByIdWithCollectionsAsync(Guid id)
|
||||
{
|
||||
var organizationUserUserDetails = await GetDetailsByIdAsync(id);
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
@ -265,7 +265,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
HidePasswords = cu.HidePasswords,
|
||||
Manage = cu.Manage
|
||||
}).ToListAsync();
|
||||
return new Tuple<OrganizationUserUserDetails, ICollection<CollectionAccessSelection>>(organizationUserUserDetails, collections);
|
||||
return (organizationUserUserDetails, collections);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -274,6 +274,11 @@ public static class ServiceCollectionExtensions
|
||||
services.AddKeyedSingleton<IPushNotificationService, RelayPushNotificationService>("implementation");
|
||||
services.AddSingleton<IPushRegistrationService, RelayPushRegistrationService>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
|
||||
}
|
||||
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))
|
||||
{
|
||||
@ -290,10 +295,6 @@ public static class ServiceCollectionExtensions
|
||||
services.AddKeyedSingleton<IPushNotificationService, AzureQueuePushNotificationService>("implementation");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
|
||||
}
|
||||
|
||||
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Mail.ConnectionString))
|
||||
{
|
||||
|
@ -0,0 +1,251 @@
|
||||
using Bit.Admin.AdminConsole.Controllers;
|
||||
using Bit.Admin.AdminConsole.Models;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReceivedExtensions;
|
||||
|
||||
namespace Admin.Test.AdminConsole.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(ProvidersController))]
|
||||
[SutProviderCustomize]
|
||||
public class ProvidersControllerTests
|
||||
{
|
||||
#region CreateMspAsync
|
||||
[BitAutoData]
|
||||
[SutProviderCustomize]
|
||||
[Theory]
|
||||
public async Task CreateMspAsync_WithValidModel_CreatesProvider(
|
||||
CreateMspProviderModel model,
|
||||
SutProvider<ProvidersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var actual = await sutProvider.Sut.CreateMsp(model);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actual);
|
||||
await sutProvider.GetDependency<ICreateProviderCommand>()
|
||||
.Received(Quantity.Exactly(1))
|
||||
.CreateMspAsync(
|
||||
Arg.Is<Provider>(x => x.Type == ProviderType.Msp),
|
||||
model.OwnerEmail,
|
||||
model.TeamsMonthlySeatMinimum,
|
||||
model.EnterpriseMonthlySeatMinimum);
|
||||
}
|
||||
|
||||
[BitAutoData]
|
||||
[SutProviderCustomize]
|
||||
[Theory]
|
||||
public async Task CreateMspAsync_RedirectsToExpectedPage_AfterCreatingProvider(
|
||||
CreateMspProviderModel model,
|
||||
Guid expectedProviderId,
|
||||
SutProvider<ProvidersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<ICreateProviderCommand>()
|
||||
.When(x =>
|
||||
x.CreateMspAsync(
|
||||
Arg.Is<Provider>(y => y.Type == ProviderType.Msp),
|
||||
model.OwnerEmail,
|
||||
model.TeamsMonthlySeatMinimum,
|
||||
model.EnterpriseMonthlySeatMinimum))
|
||||
.Do(callInfo =>
|
||||
{
|
||||
var providerArgument = callInfo.ArgAt<Provider>(0);
|
||||
providerArgument.Id = expectedProviderId;
|
||||
});
|
||||
|
||||
// Act
|
||||
var actual = await sutProvider.Sut.CreateMsp(model);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actual);
|
||||
Assert.IsType<RedirectToActionResult>(actual);
|
||||
var actualResult = (RedirectToActionResult)actual;
|
||||
Assert.Equal("Edit", actualResult.ActionName);
|
||||
Assert.Null(actualResult.ControllerName);
|
||||
Assert.Equal(expectedProviderId, actualResult.RouteValues["Id"]);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CreateMultiOrganizationEnterpriseAsync
|
||||
[BitAutoData]
|
||||
[SutProviderCustomize]
|
||||
[Theory]
|
||||
public async Task CreateMultiOrganizationEnterpriseAsync_WithValidModel_CreatesProvider(
|
||||
CreateMultiOrganizationEnterpriseProviderModel model,
|
||||
SutProvider<ProvidersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var actual = await sutProvider.Sut.CreateMultiOrganizationEnterprise(model);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actual);
|
||||
await sutProvider.GetDependency<ICreateProviderCommand>()
|
||||
.Received(Quantity.Exactly(1))
|
||||
.CreateMultiOrganizationEnterpriseAsync(
|
||||
Arg.Is<Provider>(x => x.Type == ProviderType.MultiOrganizationEnterprise),
|
||||
model.OwnerEmail,
|
||||
Arg.Is<PlanType>(y => y == model.Plan),
|
||||
model.EnterpriseSeatMinimum);
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.Received(Quantity.Exactly(1))
|
||||
.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises);
|
||||
}
|
||||
|
||||
[BitAutoData]
|
||||
[SutProviderCustomize]
|
||||
[Theory]
|
||||
public async Task CreateMultiOrganizationEnterpriseAsync_RedirectsToExpectedPage_AfterCreatingProvider(
|
||||
CreateMultiOrganizationEnterpriseProviderModel model,
|
||||
Guid expectedProviderId,
|
||||
SutProvider<ProvidersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<ICreateProviderCommand>()
|
||||
.When(x =>
|
||||
x.CreateMultiOrganizationEnterpriseAsync(
|
||||
Arg.Is<Provider>(y => y.Type == ProviderType.MultiOrganizationEnterprise),
|
||||
model.OwnerEmail,
|
||||
Arg.Is<PlanType>(y => y == model.Plan),
|
||||
model.EnterpriseSeatMinimum))
|
||||
.Do(callInfo =>
|
||||
{
|
||||
var providerArgument = callInfo.ArgAt<Provider>(0);
|
||||
providerArgument.Id = expectedProviderId;
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var actual = await sutProvider.Sut.CreateMultiOrganizationEnterprise(model);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actual);
|
||||
Assert.IsType<RedirectToActionResult>(actual);
|
||||
var actualResult = (RedirectToActionResult)actual;
|
||||
Assert.Equal("Edit", actualResult.ActionName);
|
||||
Assert.Null(actualResult.ControllerName);
|
||||
Assert.Equal(expectedProviderId, actualResult.RouteValues["Id"]);
|
||||
}
|
||||
|
||||
[BitAutoData]
|
||||
[SutProviderCustomize]
|
||||
[Theory]
|
||||
public async Task CreateMultiOrganizationEnterpriseAsync_ChecksFeatureFlag(
|
||||
CreateMultiOrganizationEnterpriseProviderModel model,
|
||||
SutProvider<ProvidersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.CreateMultiOrganizationEnterprise(model);
|
||||
|
||||
// Assert
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.Received(Quantity.Exactly(1))
|
||||
.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises);
|
||||
}
|
||||
|
||||
[BitAutoData]
|
||||
[SutProviderCustomize]
|
||||
[Theory]
|
||||
public async Task CreateMultiOrganizationEnterpriseAsync_RedirectsToProviderTypeSelectionPage_WhenFeatureFlagIsDisabled(
|
||||
CreateMultiOrganizationEnterpriseProviderModel model,
|
||||
SutProvider<ProvidersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises)
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var actual = await sutProvider.Sut.CreateMultiOrganizationEnterprise(model);
|
||||
|
||||
// Assert
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.Received(Quantity.Exactly(1))
|
||||
.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises);
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(actual);
|
||||
var actualResult = (RedirectToActionResult)actual;
|
||||
Assert.Equal("Create", actualResult.ActionName);
|
||||
Assert.Null(actualResult.ControllerName);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region CreateResellerAsync
|
||||
[BitAutoData]
|
||||
[SutProviderCustomize]
|
||||
[Theory]
|
||||
public async Task CreateResellerAsync_WithValidModel_CreatesProvider(
|
||||
CreateResellerProviderModel model,
|
||||
SutProvider<ProvidersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var actual = await sutProvider.Sut.CreateReseller(model);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actual);
|
||||
await sutProvider.GetDependency<ICreateProviderCommand>()
|
||||
.Received(Quantity.Exactly(1))
|
||||
.CreateResellerAsync(
|
||||
Arg.Is<Provider>(x => x.Type == ProviderType.Reseller));
|
||||
}
|
||||
|
||||
[BitAutoData]
|
||||
[SutProviderCustomize]
|
||||
[Theory]
|
||||
public async Task CreateResellerAsync_RedirectsToExpectedPage_AfterCreatingProvider(
|
||||
CreateResellerProviderModel model,
|
||||
Guid expectedProviderId,
|
||||
SutProvider<ProvidersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<ICreateProviderCommand>()
|
||||
.When(x =>
|
||||
x.CreateResellerAsync(
|
||||
Arg.Is<Provider>(y => y.Type == ProviderType.Reseller)))
|
||||
.Do(callInfo =>
|
||||
{
|
||||
var providerArgument = callInfo.ArgAt<Provider>(0);
|
||||
providerArgument.Id = expectedProviderId;
|
||||
});
|
||||
|
||||
// Act
|
||||
var actual = await sutProvider.Sut.CreateReseller(model);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actual);
|
||||
Assert.IsType<RedirectToActionResult>(actual);
|
||||
var actualResult = (RedirectToActionResult)actual;
|
||||
Assert.Equal("Edit", actualResult.ActionName);
|
||||
Assert.Null(actualResult.ControllerName);
|
||||
Assert.Equal(expectedProviderId, actualResult.RouteValues["Id"]);
|
||||
}
|
||||
#endregion
|
||||
}
|
@ -3,6 +3,7 @@ using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
@ -15,6 +16,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
@ -185,14 +187,46 @@ public class OrganizationUsersControllerTests
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Invite(organizationAbility.Id, model));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(true)]
|
||||
[BitAutoData(false)]
|
||||
public async Task Get_ReturnsUser(
|
||||
bool accountDeprovisioningEnabled,
|
||||
OrganizationUserUserDetails organizationUser, ICollection<CollectionAccessSelection> collections,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
organizationUser.Permissions = null;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(accountDeprovisioningEnabled);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ManageUsers(organizationUser.OrganizationId)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetDetailsByIdWithCollectionsAsync(organizationUser.Id)
|
||||
.Returns((organizationUser, collections));
|
||||
|
||||
sutProvider.GetDependency<IGetOrganizationUsersManagementStatusQuery>()
|
||||
.GetUsersOrganizationManagementStatusAsync(organizationUser.OrganizationId, Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)))
|
||||
.Returns(new Dictionary<Guid, bool> { { organizationUser.Id, true } });
|
||||
|
||||
var response = await sutProvider.Sut.Get(organizationUser.Id, false);
|
||||
|
||||
Assert.Equal(organizationUser.Id, response.Id);
|
||||
Assert.Equal(accountDeprovisioningEnabled, response.ManagedByOrganization);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Get_ReturnsUsers(
|
||||
public async Task GetMany_ReturnsUsers(
|
||||
ICollection<OrganizationUserUserDetails> organizationUsers, OrganizationAbility organizationAbility,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
Get_Setup(organizationAbility, organizationUsers, sutProvider);
|
||||
var response = await sutProvider.Sut.Get(organizationAbility.Id);
|
||||
GetMany_Setup(organizationAbility, organizationUsers, sutProvider);
|
||||
var response = await sutProvider.Sut.Get(organizationAbility.Id, false, false);
|
||||
|
||||
Assert.True(response.Data.All(r => organizationUsers.Any(ou => ou.Id == r.Id)));
|
||||
}
|
||||
@ -368,7 +402,7 @@ public class OrganizationUsersControllerTests
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.BulkDeleteAccount(orgId, model));
|
||||
}
|
||||
|
||||
private void Get_Setup(OrganizationAbility organizationAbility,
|
||||
private void GetMany_Setup(OrganizationAbility organizationAbility,
|
||||
ICollection<OrganizationUserUserDetails> organizationUsers,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
|
@ -2,7 +2,6 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@ -24,7 +23,6 @@ public class OrganizationUserUserDetailsAuthorizationHandlerTests
|
||||
CurrentContextOrganization organization,
|
||||
SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)
|
||||
{
|
||||
EnableFeatureFlag(sutProvider);
|
||||
organization.Type = userType;
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
|
||||
@ -48,7 +46,6 @@ public class OrganizationUserUserDetailsAuthorizationHandlerTests
|
||||
CurrentContextOrganization organization,
|
||||
SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)
|
||||
{
|
||||
EnableFeatureFlag(sutProvider);
|
||||
organization.Type = OrganizationUserType.User;
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ProviderUserForOrgAsync(organization.Id)
|
||||
@ -69,7 +66,6 @@ public class OrganizationUserUserDetailsAuthorizationHandlerTests
|
||||
CurrentContextOrganization organization,
|
||||
SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)
|
||||
{
|
||||
EnableFeatureFlag(sutProvider);
|
||||
organization.Type = OrganizationUserType.User;
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns(organization);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||
@ -88,78 +84,6 @@ public class OrganizationUserUserDetailsAuthorizationHandlerTests
|
||||
public async Task ReadAll_NotMember_NoSuccess(
|
||||
CurrentContextOrganization organization,
|
||||
SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)
|
||||
{
|
||||
EnableFeatureFlag(sutProvider);
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { OrganizationUserUserDetailsOperations.ReadAll },
|
||||
new ClaimsPrincipal(),
|
||||
new OrganizationScope(organization.Id)
|
||||
);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
Assert.False(context.HasSucceeded);
|
||||
}
|
||||
|
||||
private void EnableFeatureFlag(SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.Pm3478RefactorOrganizationUserApi)
|
||||
.Returns(true);
|
||||
}
|
||||
|
||||
// TESTS WITH FLAG DISABLED - TO BE DELETED IN FLAG CLEANUP
|
||||
|
||||
[Theory, CurrentContextOrganizationCustomize]
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
[BitAutoData(OrganizationUserType.User)]
|
||||
[BitAutoData(OrganizationUserType.Custom)]
|
||||
public async Task FlagDisabled_ReadAll_AnyMemberOfOrg_Success(
|
||||
OrganizationUserType userType,
|
||||
Guid userId, SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider,
|
||||
CurrentContextOrganization organization)
|
||||
{
|
||||
organization.Type = userType;
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { OrganizationUserUserDetailsOperations.ReadAll },
|
||||
new ClaimsPrincipal(),
|
||||
new OrganizationScope(organization.Id));
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CurrentContextOrganizationCustomize]
|
||||
public async Task FlagDisabled_ReadAll_ProviderUser_Success(
|
||||
CurrentContextOrganization organization,
|
||||
SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)
|
||||
{
|
||||
organization.Type = OrganizationUserType.User;
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ProviderUserForOrgAsync(organization.Id)
|
||||
.Returns(true);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { OrganizationUserUserDetailsOperations.ReadAll },
|
||||
new ClaimsPrincipal(),
|
||||
new OrganizationScope(organization.Id));
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task FlagDisabled_ReadAll_NotMember_NoSuccess(
|
||||
CurrentContextOrganization organization,
|
||||
SutProvider<OrganizationUserUserDetailsAuthorizationHandler> sutProvider)
|
||||
{
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { OrganizationUserUserDetailsOperations.ReadAll },
|
||||
|
@ -0,0 +1,57 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class OrganizationHasVerifiedDomainsQueryTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task HasVerifiedDomainsAsync_WithVerifiedDomain_ReturnsTrue(
|
||||
OrganizationDomain organizationDomain,
|
||||
SutProvider<OrganizationHasVerifiedDomainsQuery> sutProvider)
|
||||
{
|
||||
organizationDomain.SetVerifiedDate(); // Set the verified date to make it verified
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId)
|
||||
.Returns(new List<OrganizationDomain> { organizationDomain });
|
||||
|
||||
var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId);
|
||||
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HasVerifiedDomainsAsync_WithoutVerifiedDomain_ReturnsFalse(
|
||||
OrganizationDomain organizationDomain,
|
||||
SutProvider<OrganizationHasVerifiedDomainsQuery> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId)
|
||||
.Returns(new List<OrganizationDomain> { organizationDomain });
|
||||
|
||||
var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HasVerifiedDomainsAsync_WithoutOrganizationDomains_ReturnsFalse(
|
||||
Guid organizationId,
|
||||
SutProvider<OrganizationHasVerifiedDomainsQuery> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetDomainsByOrganizationIdAsync(organizationId)
|
||||
.Returns(new List<OrganizationDomain>());
|
||||
|
||||
var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationId);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
}
|
@ -1,4 +1,7 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -15,7 +18,7 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
public class VerifyOrganizationDomainCommandTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id,
|
||||
public async Task UserVerifyOrganizationDomainAsync_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id,
|
||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var expected = new OrganizationDomain
|
||||
@ -37,7 +40,7 @@ public class VerifyOrganizationDomainCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id,
|
||||
public async Task UserVerifyOrganizationDomainAsync_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id,
|
||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var expected = new OrganizationDomain
|
||||
@ -61,7 +64,7 @@ public class VerifyOrganizationDomainCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomain_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id,
|
||||
public async Task UserVerifyOrganizationDomainAsync_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id,
|
||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var expected = new OrganizationDomain
|
||||
@ -91,7 +94,7 @@ public class VerifyOrganizationDomainCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomain_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id,
|
||||
public async Task UserVerifyOrganizationDomainAsync_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id,
|
||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var expected = new OrganizationDomain
|
||||
@ -120,7 +123,7 @@ public class VerifyOrganizationDomainCommandTests
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SystemVerifyOrganizationDomain_CallsEventServiceWithUpdatedJobRunCount(SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
public async Task SystemVerifyOrganizationDomainAsync_CallsEventServiceWithUpdatedJobRunCount(SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var domain = new OrganizationDomain()
|
||||
{
|
||||
@ -137,4 +140,97 @@ public class VerifyOrganizationDomainCommandTests
|
||||
.LogOrganizationDomainEventAsync(default, EventType.OrganizationDomain_NotVerified,
|
||||
EventSystemUser.DomainVerification);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeEnabled(
|
||||
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
|
||||
.Returns([]);
|
||||
|
||||
sutProvider.GetDependency<IDnsResolverService>()
|
||||
.ResolveAsync(domain.DomainName, domain.Txt)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
|
||||
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
|
||||
|
||||
await sutProvider.GetDependency<IPolicyService>()
|
||||
.Received(1)
|
||||
.SaveAsync(Arg.Is<Policy>(x => x.Type == PolicyType.SingleOrg && x.OrganizationId == domain.OrganizationId && x.Enabled), null);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningDisabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeNotBeEnabled(
|
||||
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
|
||||
.Returns([]);
|
||||
|
||||
sutProvider.GetDependency<IDnsResolverService>()
|
||||
.ResolveAsync(domain.DomainName, domain.Txt)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(false);
|
||||
|
||||
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
|
||||
|
||||
await sutProvider.GetDependency<IPolicyService>()
|
||||
.DidNotReceive()
|
||||
.SaveAsync(Arg.Any<Policy>(), null);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldNotBeEnabled(
|
||||
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
|
||||
.Returns([]);
|
||||
|
||||
sutProvider.GetDependency<IDnsResolverService>()
|
||||
.ResolveAsync(domain.DomainName, domain.Txt)
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
|
||||
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
|
||||
|
||||
await sutProvider.GetDependency<IPolicyService>()
|
||||
.DidNotReceive()
|
||||
.SaveAsync(Arg.Any<Policy>(), null);
|
||||
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningDisabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldBeNotBeEnabled(
|
||||
OrganizationDomain domain, SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetClaimedDomainsByDomainNameAsync(domain.DomainName)
|
||||
.Returns([]);
|
||||
|
||||
sutProvider.GetDependency<IDnsResolverService>()
|
||||
.ResolveAsync(domain.DomainName, domain.Txt)
|
||||
.Returns(false);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
|
||||
_ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain);
|
||||
|
||||
await sutProvider.GetDependency<IPolicyService>()
|
||||
.DidNotReceive()
|
||||
.SaveAsync(Arg.Any<Policy>(), null);
|
||||
}
|
||||
}
|
||||
|
@ -76,48 +76,4 @@ public class OrganizationDomainServiceTests
|
||||
await sutProvider.GetDependency<IOrganizationDomainRepository>().ReceivedWithAnyArgs(1)
|
||||
.DeleteExpiredAsync(7);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HasVerifiedDomainsAsync_WithVerifiedDomain_ReturnsTrue(
|
||||
OrganizationDomain organizationDomain,
|
||||
SutProvider<OrganizationDomainService> sutProvider)
|
||||
{
|
||||
organizationDomain.SetVerifiedDate(); // Set the verified date to make it verified
|
||||
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId)
|
||||
.Returns(new List<OrganizationDomain> { organizationDomain });
|
||||
|
||||
var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId);
|
||||
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HasVerifiedDomainsAsync_WithoutVerifiedDomain_ReturnsFalse(
|
||||
OrganizationDomain organizationDomain,
|
||||
SutProvider<OrganizationDomainService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetDomainsByOrganizationIdAsync(organizationDomain.OrganizationId)
|
||||
.Returns(new List<OrganizationDomain> { organizationDomain });
|
||||
|
||||
var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationDomain.OrganizationId);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HasVerifiedDomainsAsync_WithoutOrganizationDomains_ReturnsFalse(
|
||||
Guid organizationId,
|
||||
SutProvider<OrganizationDomainService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetDomainsByOrganizationIdAsync(organizationId)
|
||||
.Returns(new List<OrganizationDomain>());
|
||||
|
||||
var result = await sutProvider.Sut.HasVerifiedDomainsAsync(organizationId);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services.Implementations;
|
||||
@ -815,4 +816,32 @@ public class PolicyServiceTests
|
||||
new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.DisableSend, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Invited, IsProvider = true }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task SaveAsync_GivenOrganizationUsingPoliciesAndHasVerifiedDomains_WhenSingleOrgPolicyIsDisabled_ThenAnErrorShouldBeThrownOrganizationHasVerifiedDomains(
|
||||
[AdminConsoleFixtures.Policy(PolicyType.SingleOrg)] Policy policy, Organization org, SutProvider<PolicyService> sutProvider)
|
||||
{
|
||||
org.Id = policy.OrganizationId;
|
||||
org.UsePolicies = true;
|
||||
|
||||
policy.Enabled = false;
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(policy.OrganizationId)
|
||||
.Returns(org);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationHasVerifiedDomainsQuery>()
|
||||
.HasVerifiedDomainsAsync(org.Id)
|
||||
.Returns(true);
|
||||
|
||||
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.SaveAsync(policy, null));
|
||||
|
||||
Assert.Equal("Organization has verified domains.", badRequestException.Message);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,173 @@
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Auth.Models.Api.Request.Accounts;
|
||||
|
||||
public class RegisterFinishRequestModelTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void GetTokenType_Returns_EmailVerification(string email, string masterPasswordHash,
|
||||
string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string emailVerificationToken)
|
||||
{
|
||||
// Arrange
|
||||
var model = new RegisterFinishRequestModel
|
||||
{
|
||||
Email = email,
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
UserSymmetricKey = userSymmetricKey,
|
||||
UserAsymmetricKeys = userAsymmetricKeys,
|
||||
Kdf = kdf,
|
||||
KdfIterations = kdfIterations,
|
||||
EmailVerificationToken = emailVerificationToken
|
||||
};
|
||||
|
||||
// Act
|
||||
Assert.Equal(RegisterFinishTokenType.EmailVerification, model.GetTokenType());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void GetTokenType_Returns_OrganizationInvite(string email, string masterPasswordHash,
|
||||
string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string orgInviteToken, Guid organizationUserId)
|
||||
{
|
||||
// Arrange
|
||||
var model = new RegisterFinishRequestModel
|
||||
{
|
||||
Email = email,
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
UserSymmetricKey = userSymmetricKey,
|
||||
UserAsymmetricKeys = userAsymmetricKeys,
|
||||
Kdf = kdf,
|
||||
KdfIterations = kdfIterations,
|
||||
OrgInviteToken = orgInviteToken,
|
||||
OrganizationUserId = organizationUserId
|
||||
};
|
||||
|
||||
// Act
|
||||
Assert.Equal(RegisterFinishTokenType.OrganizationInvite, model.GetTokenType());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void GetTokenType_Returns_OrgSponsoredFreeFamilyPlan(string email, string masterPasswordHash,
|
||||
string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string orgSponsoredFreeFamilyPlanToken)
|
||||
{
|
||||
// Arrange
|
||||
var model = new RegisterFinishRequestModel
|
||||
{
|
||||
Email = email,
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
UserSymmetricKey = userSymmetricKey,
|
||||
UserAsymmetricKeys = userAsymmetricKeys,
|
||||
Kdf = kdf,
|
||||
KdfIterations = kdfIterations,
|
||||
OrgSponsoredFreeFamilyPlanToken = orgSponsoredFreeFamilyPlanToken
|
||||
};
|
||||
|
||||
// Act
|
||||
Assert.Equal(RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan, model.GetTokenType());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void GetTokenType_Returns_EmergencyAccessInvite(string email, string masterPasswordHash,
|
||||
string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
|
||||
{
|
||||
// Arrange
|
||||
var model = new RegisterFinishRequestModel
|
||||
{
|
||||
Email = email,
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
UserSymmetricKey = userSymmetricKey,
|
||||
UserAsymmetricKeys = userAsymmetricKeys,
|
||||
Kdf = kdf,
|
||||
KdfIterations = kdfIterations,
|
||||
AcceptEmergencyAccessInviteToken = acceptEmergencyAccessInviteToken,
|
||||
AcceptEmergencyAccessId = acceptEmergencyAccessId
|
||||
};
|
||||
|
||||
// Act
|
||||
Assert.Equal(RegisterFinishTokenType.EmergencyAccessInvite, model.GetTokenType());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void GetTokenType_Returns_ProviderInvite(string email, string masterPasswordHash,
|
||||
string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations, string providerInviteToken, Guid providerUserId)
|
||||
{
|
||||
// Arrange
|
||||
var model = new RegisterFinishRequestModel
|
||||
{
|
||||
Email = email,
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
UserSymmetricKey = userSymmetricKey,
|
||||
UserAsymmetricKeys = userAsymmetricKeys,
|
||||
Kdf = kdf,
|
||||
KdfIterations = kdfIterations,
|
||||
ProviderInviteToken = providerInviteToken,
|
||||
ProviderUserId = providerUserId
|
||||
};
|
||||
|
||||
// Act
|
||||
Assert.Equal(RegisterFinishTokenType.ProviderInvite, model.GetTokenType());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void GetTokenType_Returns_Invalid(string email, string masterPasswordHash,
|
||||
string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations)
|
||||
{
|
||||
// Arrange
|
||||
var model = new RegisterFinishRequestModel
|
||||
{
|
||||
Email = email,
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
UserSymmetricKey = userSymmetricKey,
|
||||
UserAsymmetricKeys = userAsymmetricKeys,
|
||||
Kdf = kdf,
|
||||
KdfIterations = kdfIterations
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = Assert.Throws<InvalidOperationException>(() => model.GetTokenType());
|
||||
Assert.Equal("Invalid token type.", result.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void ToUser_Returns_User(string email, string masterPasswordHash, string masterPasswordHint,
|
||||
string userSymmetricKey, KeysRequestModel userAsymmetricKeys, KdfType kdf, int kdfIterations,
|
||||
int? kdfMemory, int? kdfParallelism)
|
||||
{
|
||||
// Arrange
|
||||
var model = new RegisterFinishRequestModel
|
||||
{
|
||||
Email = email,
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
MasterPasswordHint = masterPasswordHint,
|
||||
UserSymmetricKey = userSymmetricKey,
|
||||
UserAsymmetricKeys = userAsymmetricKeys,
|
||||
Kdf = kdf,
|
||||
KdfIterations = kdfIterations,
|
||||
KdfMemory = kdfMemory,
|
||||
KdfParallelism = kdfParallelism
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = model.ToUser();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(email, result.Email);
|
||||
Assert.Equal(masterPasswordHint, result.MasterPasswordHint);
|
||||
Assert.Equal(kdf, result.Kdf);
|
||||
Assert.Equal(kdfIterations, result.KdfIterations);
|
||||
Assert.Equal(kdfMemory, result.KdfMemory);
|
||||
Assert.Equal(kdfParallelism, result.KdfParallelism);
|
||||
Assert.Equal(userSymmetricKey, result.Key);
|
||||
Assert.Equal(userAsymmetricKeys.PublicKey, result.PublicKey);
|
||||
Assert.Equal(userAsymmetricKeys.EncryptedPrivateKey, result.PrivateKey);
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using System.Text;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Entities;
|
||||
@ -19,7 +20,9 @@ using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
@ -28,8 +31,10 @@ namespace Bit.Core.Test.Auth.UserFeatures.Registration;
|
||||
[SutProviderCustomize]
|
||||
public class RegisterUserCommandTests
|
||||
{
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// RegisterUser tests
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUser_Succeeds(SutProvider<RegisterUserCommand> sutProvider, User user)
|
||||
@ -86,7 +91,10 @@ public class RegisterUserCommandTests
|
||||
.RaiseEventAsync(Arg.Any<ReferenceEvent>());
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// RegisterUserWithOrganizationInviteToken tests
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
// Simple happy path test
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
@ -312,7 +320,10 @@ public class RegisterUserCommandTests
|
||||
Assert.Equal(expectedErrorMessage, exception.Message);
|
||||
}
|
||||
|
||||
// RegisterUserViaEmailVerificationToken
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// RegisterUserViaEmailVerificationToken tests
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaEmailVerificationToken_Succeeds(SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash, string emailVerificationToken, bool receiveMarketingMaterials)
|
||||
@ -382,10 +393,9 @@ public class RegisterUserCommandTests
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken tests
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
@ -452,7 +462,9 @@ public class RegisterUserCommandTests
|
||||
Assert.Equal("Open registration has been disabled by the system administrator.", result.Message);
|
||||
}
|
||||
|
||||
// RegisterUserViaAcceptEmergencyAccessInviteToken
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// RegisterUserViaAcceptEmergencyAccessInviteToken tests
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
@ -495,8 +507,6 @@ public class RegisterUserCommandTests
|
||||
.RaiseEventAsync(Arg.Is<ReferenceEvent>(refEvent => refEvent.Type == ReferenceEventType.Signup));
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_InvalidToken_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user,
|
||||
@ -536,5 +546,140 @@ public class RegisterUserCommandTests
|
||||
Assert.Equal("Open registration has been disabled by the system administrator.", result.Message);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// RegisterUserViaProviderInviteToken tests
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaProviderInviteToken_Succeeds(SutProvider<RegisterUserCommand> sutProvider,
|
||||
User user, string masterPasswordHash, Guid providerUserId)
|
||||
{
|
||||
// Arrange
|
||||
// Start with plaintext
|
||||
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
||||
var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}";
|
||||
|
||||
// Get the byte array of the plaintext
|
||||
var decryptedProviderInviteTokenByteArray = Encoding.UTF8.GetBytes(decryptedProviderInviteToken);
|
||||
|
||||
// Base64 encode the byte array (this is passed to protector.protect(bytes))
|
||||
var base64EncodedProviderInvToken = WebEncoders.Base64UrlEncode(decryptedProviderInviteTokenByteArray);
|
||||
|
||||
var mockDataProtector = Substitute.For<IDataProtector>();
|
||||
|
||||
// Given any byte array, just return the decryptedProviderInviteTokenByteArray (sidestepping any actual encryption)
|
||||
mockDataProtector.Unprotect(Arg.Any<byte[]>()).Returns(decryptedProviderInviteTokenByteArray);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectionProvider>()
|
||||
.CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(mockDataProtector);
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>()
|
||||
.OrganizationInviteExpirationHours.Returns(120); // 5 days
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CreateUserAsync(user, masterPasswordHash)
|
||||
.Returns(IdentityResult.Success);
|
||||
|
||||
// Using sutProvider in the parameters of the function means that the constructor has already run for the
|
||||
// command so we have to recreate it in order for our mock overrides to be used.
|
||||
sutProvider.Create();
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.RegisterUserViaProviderInviteToken(user, masterPasswordHash, base64EncodedProviderInvToken, providerUserId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Succeeded);
|
||||
|
||||
await sutProvider.GetDependency<IUserService>()
|
||||
.Received(1)
|
||||
.CreateUserAsync(Arg.Is<User>(u => u.Name == user.Name && u.EmailVerified == true && u.ApiKey != null), masterPasswordHash);
|
||||
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendWelcomeEmailAsync(user);
|
||||
|
||||
await sutProvider.GetDependency<IReferenceEventService>()
|
||||
.Received(1)
|
||||
.RaiseEventAsync(Arg.Is<ReferenceEvent>(refEvent => refEvent.Type == ReferenceEventType.Signup));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaProviderInviteToken_InvalidToken_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider,
|
||||
User user, string masterPasswordHash, Guid providerUserId)
|
||||
{
|
||||
// Arrange
|
||||
// Start with plaintext
|
||||
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
||||
var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}";
|
||||
|
||||
// Get the byte array of the plaintext
|
||||
var decryptedProviderInviteTokenByteArray = Encoding.UTF8.GetBytes(decryptedProviderInviteToken);
|
||||
|
||||
// Base64 encode the byte array (this is passed to protector.protect(bytes))
|
||||
var base64EncodedProviderInvToken = WebEncoders.Base64UrlEncode(decryptedProviderInviteTokenByteArray);
|
||||
|
||||
var mockDataProtector = Substitute.For<IDataProtector>();
|
||||
|
||||
// Given any byte array, just return the decryptedProviderInviteTokenByteArray (sidestepping any actual encryption)
|
||||
mockDataProtector.Unprotect(Arg.Any<byte[]>()).Returns(decryptedProviderInviteTokenByteArray);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectionProvider>()
|
||||
.CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(mockDataProtector);
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>()
|
||||
.OrganizationInviteExpirationHours.Returns(120); // 5 days
|
||||
|
||||
// Using sutProvider in the parameters of the function means that the constructor has already run for the
|
||||
// command so we have to recreate it in order for our mock overrides to be used.
|
||||
sutProvider.Create();
|
||||
|
||||
// Act & Assert
|
||||
var result = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RegisterUserViaProviderInviteToken(user, masterPasswordHash, base64EncodedProviderInvToken, Guid.NewGuid()));
|
||||
Assert.Equal("Invalid provider invite token.", result.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RegisterUserViaProviderInviteToken_DisabledOpenRegistration_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider,
|
||||
User user, string masterPasswordHash, Guid providerUserId)
|
||||
{
|
||||
// Arrange
|
||||
// Start with plaintext
|
||||
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
||||
var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {user.Email} {nowMillis}";
|
||||
|
||||
// Get the byte array of the plaintext
|
||||
var decryptedProviderInviteTokenByteArray = Encoding.UTF8.GetBytes(decryptedProviderInviteToken);
|
||||
|
||||
// Base64 encode the byte array (this is passed to protector.protect(bytes))
|
||||
var base64EncodedProviderInvToken = WebEncoders.Base64UrlEncode(decryptedProviderInviteTokenByteArray);
|
||||
|
||||
var mockDataProtector = Substitute.For<IDataProtector>();
|
||||
|
||||
// Given any byte array, just return the decryptedProviderInviteTokenByteArray (sidestepping any actual encryption)
|
||||
mockDataProtector.Unprotect(Arg.Any<byte[]>()).Returns(decryptedProviderInviteTokenByteArray);
|
||||
|
||||
sutProvider.GetDependency<IDataProtectionProvider>()
|
||||
.CreateProtector("ProviderServiceDataProtector")
|
||||
.Returns(mockDataProtector);
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>()
|
||||
.DisableUserRegistration = true;
|
||||
|
||||
// Using sutProvider in the parameters of the function means that the constructor has already run for the
|
||||
// command so we have to recreate it in order for our mock overrides to be used.
|
||||
sutProvider.Create();
|
||||
|
||||
// Act & Assert
|
||||
var result = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||
sutProvider.Sut.RegisterUserViaProviderInviteToken(user, masterPasswordHash, base64EncodedProviderInvToken, providerUserId));
|
||||
Assert.Equal("Open registration has been disabled by the system administrator.", result.Message);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
@ -9,10 +10,12 @@ using Bit.Core.Models.Business.Tokenables;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Identity.Models.Request.Accounts;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
@ -470,6 +473,80 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
|
||||
Assert.Equal(kdfParallelism, user.KdfParallelism);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RegistrationWithEmailVerification_WithProviderInviteToken_Succeeds(
|
||||
[StringLength(1000)] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, string userSymmetricKey,
|
||||
KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism)
|
||||
{
|
||||
|
||||
// Localize factory to just this test.
|
||||
var localFactory = new IdentityApplicationFactory();
|
||||
|
||||
// Hardcoded, valid data
|
||||
var email = "jsnider+local253@bitwarden.com";
|
||||
var providerUserId = new Guid("c6fdba35-2e52-43b4-8fb7-b211011d154a");
|
||||
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
||||
var decryptedProviderInviteToken = $"ProviderUserInvite {providerUserId} {email} {nowMillis}";
|
||||
// var providerInviteToken = await GetValidProviderInviteToken(localFactory, email, providerUserId);
|
||||
|
||||
// Get the byte array of the plaintext
|
||||
var decryptedProviderInviteTokenByteArray = Encoding.UTF8.GetBytes(decryptedProviderInviteToken);
|
||||
|
||||
// Base64 encode the byte array (this is passed to protector.protect(bytes))
|
||||
var base64EncodedProviderInvToken = WebEncoders.Base64UrlEncode(decryptedProviderInviteTokenByteArray);
|
||||
|
||||
var mockDataProtector = Substitute.For<IDataProtector>();
|
||||
mockDataProtector.Unprotect(Arg.Any<byte[]>()).Returns(decryptedProviderInviteTokenByteArray);
|
||||
|
||||
localFactory.SubstituteService<IDataProtectionProvider>(dataProtectionProvider =>
|
||||
{
|
||||
dataProtectionProvider.CreateProtector(Arg.Any<string>())
|
||||
.Returns(mockDataProtector);
|
||||
});
|
||||
|
||||
// As token contains now milliseconds for when it was created, create 1k year timespan for expiration
|
||||
// to ensure token is valid for a good long while.
|
||||
localFactory.UpdateConfiguration("globalSettings:OrganizationInviteExpirationHours", "8760000");
|
||||
|
||||
var registerFinishReqModel = new RegisterFinishRequestModel
|
||||
{
|
||||
Email = email,
|
||||
MasterPasswordHash = masterPasswordHash,
|
||||
MasterPasswordHint = masterPasswordHint,
|
||||
ProviderInviteToken = base64EncodedProviderInvToken,
|
||||
ProviderUserId = providerUserId,
|
||||
Kdf = KdfType.PBKDF2_SHA256,
|
||||
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
|
||||
UserSymmetricKey = userSymmetricKey,
|
||||
UserAsymmetricKeys = userAsymmetricKeys,
|
||||
KdfMemory = kdfMemory,
|
||||
KdfParallelism = kdfParallelism
|
||||
};
|
||||
|
||||
var postRegisterFinishHttpContext = await localFactory.PostRegisterFinishAsync(registerFinishReqModel);
|
||||
|
||||
Assert.Equal(StatusCodes.Status200OK, postRegisterFinishHttpContext.Response.StatusCode);
|
||||
|
||||
var database = localFactory.GetDatabaseContext();
|
||||
var user = await database.Users
|
||||
.SingleAsync(u => u.Email == email);
|
||||
|
||||
Assert.NotNull(user);
|
||||
|
||||
// Assert user properties match the request model
|
||||
Assert.Equal(email, user.Email);
|
||||
Assert.NotEqual(masterPasswordHash, user.MasterPassword); // We execute server side hashing
|
||||
Assert.NotNull(user.MasterPassword);
|
||||
Assert.Equal(masterPasswordHint, user.MasterPasswordHint);
|
||||
Assert.Equal(userSymmetricKey, user.Key);
|
||||
Assert.Equal(userAsymmetricKeys.EncryptedPrivateKey, user.PrivateKey);
|
||||
Assert.Equal(userAsymmetricKeys.PublicKey, user.PublicKey);
|
||||
Assert.Equal(KdfType.PBKDF2_SHA256, user.Kdf);
|
||||
Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, user.KdfIterations);
|
||||
Assert.Equal(kdfMemory, user.KdfMemory);
|
||||
Assert.Equal(kdfParallelism, user.KdfParallelism);
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostRegisterVerificationEmailClicked_Success(
|
||||
@ -527,4 +604,5 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Bit.Identity.IdentityServer.RequestValidators;
|
||||
using Bit.Identity.Models.Request.Accounts;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@ -237,6 +237,11 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
|
||||
MasterPasswordHash = DefaultPassword
|
||||
});
|
||||
var userManager = factory.GetService<UserManager<User>>();
|
||||
await factory.RegisterAsync(new RegisterRequestModel
|
||||
{
|
||||
Email = DefaultUsername,
|
||||
MasterPasswordHash = DefaultPassword
|
||||
});
|
||||
var user = await userManager.FindByEmailAsync(DefaultUsername);
|
||||
Assert.NotNull(user);
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@ -11,8 +10,8 @@ using Bit.Core.Models.Api;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Bit.Identity.IdentityServer.RequestValidators;
|
||||
using Bit.Identity.Test.Wrappers;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Duende.IdentityServer.Validation;
|
||||
@ -32,18 +31,14 @@ public class BaseRequestValidatorTests
|
||||
private readonly IUserService _userService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IDeviceValidator _deviceValidator;
|
||||
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
|
||||
private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly ILogger<BaseRequestValidatorTests> _logger;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _tokenDataFactory;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IUserDecryptionOptionsBuilder _userDecryptionOptionsBuilder;
|
||||
@ -52,43 +47,35 @@ public class BaseRequestValidatorTests
|
||||
|
||||
public BaseRequestValidatorTests()
|
||||
{
|
||||
_userManager = SubstituteUserManager();
|
||||
_userService = Substitute.For<IUserService>();
|
||||
_eventService = Substitute.For<IEventService>();
|
||||
_deviceValidator = Substitute.For<IDeviceValidator>();
|
||||
_organizationDuoWebTokenProvider = Substitute.For<IOrganizationDuoWebTokenProvider>();
|
||||
_duoWebV4SDKService = Substitute.For<ITemporaryDuoWebV4SDKService>();
|
||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||
_twoFactorAuthenticationValidator = Substitute.For<ITwoFactorAuthenticationValidator>();
|
||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||
_applicationCacheService = Substitute.For<IApplicationCacheService>();
|
||||
_mailService = Substitute.For<IMailService>();
|
||||
_logger = Substitute.For<ILogger<BaseRequestValidatorTests>>();
|
||||
_currentContext = Substitute.For<ICurrentContext>();
|
||||
_globalSettings = Substitute.For<GlobalSettings>();
|
||||
_userRepository = Substitute.For<IUserRepository>();
|
||||
_policyService = Substitute.For<IPolicyService>();
|
||||
_tokenDataFactory = Substitute.For<IDataProtectorTokenFactory<SsoEmail2faSessionTokenable>>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
|
||||
_userDecryptionOptionsBuilder = Substitute.For<IUserDecryptionOptionsBuilder>();
|
||||
_userManager = SubstituteUserManager();
|
||||
|
||||
_sut = new BaseRequestValidatorTestWrapper(
|
||||
_userManager,
|
||||
_userService,
|
||||
_eventService,
|
||||
_deviceValidator,
|
||||
_organizationDuoWebTokenProvider,
|
||||
_duoWebV4SDKService,
|
||||
_organizationRepository,
|
||||
_twoFactorAuthenticationValidator,
|
||||
_organizationUserRepository,
|
||||
_applicationCacheService,
|
||||
_mailService,
|
||||
_logger,
|
||||
_currentContext,
|
||||
_globalSettings,
|
||||
_userRepository,
|
||||
_policyService,
|
||||
_tokenDataFactory,
|
||||
_featureService,
|
||||
_ssoConfigRepository,
|
||||
_userDecryptionOptionsBuilder);
|
||||
@ -116,7 +103,7 @@ public class BaseRequestValidatorTests
|
||||
|
||||
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
await _eventService.Received(1)
|
||||
.LogUserEventAsync(context.CustomValidatorRequestContext.User.Id,
|
||||
Core.Enums.EventType.User_FailedLogIn);
|
||||
@ -127,7 +114,7 @@ public class BaseRequestValidatorTests
|
||||
/* Logic path
|
||||
ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
|
||||
|-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|
||||
(self hosted) |-> _logger.LogWarning()
|
||||
(self hosted) |-> _logger.LogWarning()
|
||||
|-> SetErrorResult
|
||||
*/
|
||||
[Theory, BitAutoData]
|
||||
@ -154,7 +141,7 @@ public class BaseRequestValidatorTests
|
||||
|
||||
/* Logic path
|
||||
ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
|
||||
|-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|
||||
|-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|
||||
|-> SetErrorResult
|
||||
*/
|
||||
[Theory, BitAutoData]
|
||||
@ -202,6 +189,9 @@ public class BaseRequestValidatorTests
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
_twoFactorAuthenticationValidator
|
||||
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, default)));
|
||||
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_sut.isValid = true;
|
||||
@ -230,6 +220,9 @@ public class BaseRequestValidatorTests
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
_twoFactorAuthenticationValidator
|
||||
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_sut.isValid = true;
|
||||
@ -237,7 +230,7 @@ public class BaseRequestValidatorTests
|
||||
context.CustomValidatorRequestContext.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(1);
|
||||
_globalSettings.DisableEmailNewDevice = false;
|
||||
|
||||
context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device
|
||||
context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device
|
||||
|
||||
_deviceValidator.SaveDeviceAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
|
||||
.Returns(device);
|
||||
@ -267,10 +260,13 @@ public class BaseRequestValidatorTests
|
||||
context.CustomValidatorRequestContext.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(1);
|
||||
_globalSettings.DisableEmailNewDevice = false;
|
||||
|
||||
context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device
|
||||
context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device
|
||||
|
||||
_deviceValidator.SaveDeviceAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
|
||||
.Returns(device);
|
||||
_twoFactorAuthenticationValidator
|
||||
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
@ -306,10 +302,13 @@ public class BaseRequestValidatorTests
|
||||
_policyService.AnyPoliciesApplicableToUserAsync(
|
||||
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
|
||||
.Returns(Task.FromResult(true));
|
||||
_twoFactorAuthenticationValidator
|
||||
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
// Assert
|
||||
Assert.True(context.GrantResult.IsError);
|
||||
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
|
||||
Assert.Equal("SSO authentication is required.", errorResponse.Message);
|
||||
@ -330,6 +329,9 @@ public class BaseRequestValidatorTests
|
||||
context.ValidatedTokenRequest.ClientId = "Not Web";
|
||||
_sut.isValid = true;
|
||||
_featureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers).Returns(true);
|
||||
_twoFactorAuthenticationValidator
|
||||
.RequiresTwoFactorAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
@ -341,28 +343,6 @@ public class BaseRequestValidatorTests
|
||||
, errorResponse.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RequiresTwoFactorAsync_ClientCredentialsGrantType_ShouldReturnFalse(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
context.ValidatedTokenRequest.GrantType = "client_credentials";
|
||||
|
||||
// Act
|
||||
var result = await _sut.TestRequiresTwoFactorAsync(
|
||||
context.CustomValidatorRequestContext.User,
|
||||
context.ValidatedTokenRequest);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Item1);
|
||||
Assert.Null(result.Item2);
|
||||
}
|
||||
|
||||
private BaseRequestValidationContextFake CreateContext(
|
||||
ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
|
@ -4,7 +4,7 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Bit.Identity.IdentityServer.RequestValidators;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using NSubstitute;
|
||||
|
@ -0,0 +1,575 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Identity.IdentityServer.RequestValidators;
|
||||
using Bit.Identity.Test.Wrappers;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using AuthFixtures = Bit.Identity.Test.AutoFixture;
|
||||
|
||||
namespace Bit.Identity.Test.IdentityServer;
|
||||
|
||||
public class TwoFactorAuthenticationValidatorTests
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly UserManagerTestWrapper<User> _userManager;
|
||||
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
|
||||
private readonly ITemporaryDuoWebV4SDKService _temporaryDuoWebV4SDKService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmail2faSessionTokenable;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly TwoFactorAuthenticationValidator _sut;
|
||||
|
||||
public TwoFactorAuthenticationValidatorTests()
|
||||
{
|
||||
_userService = Substitute.For<IUserService>();
|
||||
_userManager = SubstituteUserManager();
|
||||
_organizationDuoWebTokenProvider = Substitute.For<IOrganizationDuoWebTokenProvider>();
|
||||
_temporaryDuoWebV4SDKService = Substitute.For<ITemporaryDuoWebV4SDKService>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_applicationCacheService = Substitute.For<IApplicationCacheService>();
|
||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||
_ssoEmail2faSessionTokenable = Substitute.For<IDataProtectorTokenFactory<SsoEmail2faSessionTokenable>>();
|
||||
_currentContext = Substitute.For<ICurrentContext>();
|
||||
|
||||
_sut = new TwoFactorAuthenticationValidator(
|
||||
_userService,
|
||||
_userManager,
|
||||
_organizationDuoWebTokenProvider,
|
||||
_temporaryDuoWebV4SDKService,
|
||||
_featureService,
|
||||
_applicationCacheService,
|
||||
_organizationUserRepository,
|
||||
_organizationRepository,
|
||||
_ssoEmail2faSessionTokenable,
|
||||
_currentContext);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("password")]
|
||||
[BitAutoData("authorization_code")]
|
||||
public async void RequiresTwoFactorAsync_IndividualOnly_Required_ReturnTrue(
|
||||
string grantType,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
request.GrantType = grantType;
|
||||
// All three of these must be true for the two factor authentication to be required
|
||||
_userManager.TWO_FACTOR_ENABLED = true;
|
||||
_userManager.SUPPORTS_TWO_FACTOR = true;
|
||||
// In order for the two factor authentication to be required, the user must have at least one two factor provider
|
||||
_userManager.TWO_FACTOR_PROVIDERS = ["email"];
|
||||
|
||||
// Act
|
||||
var result = await _sut.RequiresTwoFactorAsync(user, request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Item1);
|
||||
Assert.Null(result.Item2);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("client_credentials")]
|
||||
[BitAutoData("webauthn")]
|
||||
public async void RequiresTwoFactorAsync_NotRequired_ReturnFalse(
|
||||
string grantType,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
request.GrantType = grantType;
|
||||
|
||||
// Act
|
||||
var result = await _sut.RequiresTwoFactorAsync(user, request);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Item1);
|
||||
Assert.Null(result.Item2);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("password")]
|
||||
[BitAutoData("authorization_code")]
|
||||
public async void RequiresTwoFactorAsync_IndividualFalse_OrganizationRequired_ReturnTrue(
|
||||
string grantType,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||
User user,
|
||||
OrganizationUserOrganizationDetails orgUser,
|
||||
Organization organization,
|
||||
ICollection<CurrentContextOrganization> organizationCollection)
|
||||
{
|
||||
// Arrange
|
||||
request.GrantType = grantType;
|
||||
// Link the orgUser to the User making the request
|
||||
orgUser.UserId = user.Id;
|
||||
// Link organization to the organization user
|
||||
organization.Id = orgUser.OrganizationId;
|
||||
|
||||
// Set Organization 2FA to required
|
||||
organization.Use2fa = true;
|
||||
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();
|
||||
organization.Enabled = true;
|
||||
|
||||
// Make sure organization list is not empty
|
||||
organizationCollection.Clear();
|
||||
// Fix OrganizationUser Permissions field
|
||||
orgUser.Permissions = "{}";
|
||||
organizationCollection.Add(new CurrentContextOrganization(orgUser));
|
||||
|
||||
_currentContext.OrganizationMembershipAsync(Arg.Any<IOrganizationUserRepository>(), Arg.Any<Guid>())
|
||||
.Returns(Task.FromResult(organizationCollection));
|
||||
|
||||
_applicationCacheService.GetOrganizationAbilitiesAsync()
|
||||
.Returns(new Dictionary<Guid, OrganizationAbility>()
|
||||
{
|
||||
{ orgUser.OrganizationId, new OrganizationAbility(organization)}
|
||||
});
|
||||
|
||||
_organizationRepository.GetManyByUserIdAsync(Arg.Any<Guid>()).Returns([organization]);
|
||||
|
||||
// Act
|
||||
var result = await _sut.RequiresTwoFactorAsync(user, request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.Item1);
|
||||
Assert.NotNull(result.Item2);
|
||||
Assert.IsType<Organization>(result.Item2);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void BuildTwoFactorResultAsync_NoProviders_ReturnsNull(
|
||||
User user,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
organization.Use2fa = true;
|
||||
organization.TwoFactorProviders = "{}";
|
||||
organization.Enabled = true;
|
||||
|
||||
user.TwoFactorProviders = "";
|
||||
|
||||
// Act
|
||||
var result = await _sut.BuildTwoFactorResultAsync(user, organization);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void BuildTwoFactorResultAsync_OrganizationProviders_NotEnabled_ReturnsNull(
|
||||
User user,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
organization.Use2fa = true;
|
||||
organization.TwoFactorProviders = GetTwoFactorOrganizationNotEnabledDuoProviderJson();
|
||||
organization.Enabled = true;
|
||||
|
||||
user.TwoFactorProviders = null;
|
||||
|
||||
// Act
|
||||
var result = await _sut.BuildTwoFactorResultAsync(user, organization);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void BuildTwoFactorResultAsync_OrganizationProviders_ReturnsNotNull(
|
||||
User user,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
organization.Use2fa = true;
|
||||
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();
|
||||
organization.Enabled = true;
|
||||
|
||||
user.TwoFactorProviders = null;
|
||||
|
||||
// Act
|
||||
var result = await _sut.BuildTwoFactorResultAsync(user, organization);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<Dictionary<string, object>>(result);
|
||||
Assert.NotEmpty(result);
|
||||
Assert.True(result.ContainsKey("TwoFactorProviders2"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void BuildTwoFactorResultAsync_IndividualProviders_NotEnabled_ReturnsNull(
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
user.TwoFactorProviders = GetTwoFactorIndividualNotEnabledProviderJson(TwoFactorProviderType.Email);
|
||||
|
||||
// Act
|
||||
var result = await _sut.BuildTwoFactorResultAsync(user, null);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void BuildTwoFactorResultAsync_IndividualProviders_ReturnsNotNull(
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
_userService.CanAccessPremium(user).Returns(true);
|
||||
|
||||
user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(TwoFactorProviderType.Duo);
|
||||
|
||||
// Act
|
||||
var result = await _sut.BuildTwoFactorResultAsync(user, null);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<Dictionary<string, object>>(result);
|
||||
Assert.NotEmpty(result);
|
||||
Assert.True(result.ContainsKey("TwoFactorProviders2"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(TwoFactorProviderType.Email)]
|
||||
public async void BuildTwoFactorResultAsync_IndividualEmailProvider_SendsEmail_SetsSsoToken_ReturnsNotNull(
|
||||
TwoFactorProviderType providerType,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
var providerTypeInt = (int)providerType;
|
||||
user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType);
|
||||
|
||||
_userManager.TWO_FACTOR_ENABLED = true;
|
||||
_userManager.SUPPORTS_TWO_FACTOR = true;
|
||||
_userManager.TWO_FACTOR_PROVIDERS = [providerType.ToString()];
|
||||
|
||||
_userService.TwoFactorProviderIsEnabledAsync(Arg.Any<TwoFactorProviderType>(), user)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await _sut.BuildTwoFactorResultAsync(user, null);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<Dictionary<string, object>>(result);
|
||||
Assert.NotEmpty(result);
|
||||
Assert.True(result.ContainsKey("TwoFactorProviders2"));
|
||||
var providers = (Dictionary<string, Dictionary<string, object>>)result["TwoFactorProviders2"];
|
||||
Assert.True(providers.ContainsKey(providerTypeInt.ToString()));
|
||||
Assert.True(result.ContainsKey("SsoEmail2faSessionToken"));
|
||||
Assert.True(result.ContainsKey("Email"));
|
||||
|
||||
await _userService.Received(1).SendTwoFactorEmailAsync(Arg.Any<User>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(TwoFactorProviderType.Duo)]
|
||||
[BitAutoData(TwoFactorProviderType.WebAuthn)]
|
||||
[BitAutoData(TwoFactorProviderType.Email)]
|
||||
[BitAutoData(TwoFactorProviderType.YubiKey)]
|
||||
[BitAutoData(TwoFactorProviderType.OrganizationDuo)]
|
||||
public async void BuildTwoFactorResultAsync_IndividualProvider_ReturnMatchesType(
|
||||
TwoFactorProviderType providerType,
|
||||
User user)
|
||||
{
|
||||
// Arrange
|
||||
var providerTypeInt = (int)providerType;
|
||||
user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType);
|
||||
|
||||
_userManager.TWO_FACTOR_ENABLED = true;
|
||||
_userManager.SUPPORTS_TWO_FACTOR = true;
|
||||
_userManager.TWO_FACTOR_PROVIDERS = [providerType.ToString()];
|
||||
_userManager.TWO_FACTOR_TOKEN = "{\"Key1\":\"WebauthnToken\"}";
|
||||
|
||||
_userService.CanAccessPremium(user).Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await _sut.BuildTwoFactorResultAsync(user, null);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<Dictionary<string, object>>(result);
|
||||
Assert.NotEmpty(result);
|
||||
Assert.True(result.ContainsKey("TwoFactorProviders2"));
|
||||
var providers = (Dictionary<string, Dictionary<string, object>>)result["TwoFactorProviders2"];
|
||||
Assert.True(providers.ContainsKey(providerTypeInt.ToString()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void VerifyTwoFactorAsync_Individual_TypeNull_ReturnsFalse(
|
||||
User user,
|
||||
string token)
|
||||
{
|
||||
// Arrange
|
||||
_userService.TwoFactorProviderIsEnabledAsync(
|
||||
TwoFactorProviderType.Email, user).Returns(true);
|
||||
|
||||
_userManager.TWO_FACTOR_PROVIDERS = ["email"];
|
||||
|
||||
// Act
|
||||
var result = await _sut.VerifyTwoFactor(
|
||||
user, null, TwoFactorProviderType.U2f, token);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void VerifyTwoFactorAsync_Individual_NotEnabled_ReturnsFalse(
|
||||
User user,
|
||||
string token)
|
||||
{
|
||||
// Arrange
|
||||
_userService.TwoFactorProviderIsEnabledAsync(
|
||||
TwoFactorProviderType.Email, user).Returns(false);
|
||||
|
||||
_userManager.TWO_FACTOR_PROVIDERS = ["email"];
|
||||
|
||||
// Act
|
||||
var result = await _sut.VerifyTwoFactor(
|
||||
user, null, TwoFactorProviderType.Email, token);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async void VerifyTwoFactorAsync_Organization_NotEnabled_ReturnsFalse(
|
||||
User user,
|
||||
string token)
|
||||
{
|
||||
// Arrange
|
||||
_userService.TwoFactorProviderIsEnabledAsync(
|
||||
TwoFactorProviderType.OrganizationDuo, user).Returns(false);
|
||||
|
||||
_userManager.TWO_FACTOR_PROVIDERS = ["OrganizationDuo"];
|
||||
|
||||
// Act
|
||||
var result = await _sut.VerifyTwoFactor(
|
||||
user, null, TwoFactorProviderType.OrganizationDuo, token);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(TwoFactorProviderType.Duo)]
|
||||
[BitAutoData(TwoFactorProviderType.WebAuthn)]
|
||||
[BitAutoData(TwoFactorProviderType.Email)]
|
||||
[BitAutoData(TwoFactorProviderType.YubiKey)]
|
||||
[BitAutoData(TwoFactorProviderType.Remember)]
|
||||
public async void VerifyTwoFactorAsync_Individual_ValidToken_ReturnsTrue(
|
||||
TwoFactorProviderType providerType,
|
||||
User user,
|
||||
string token)
|
||||
{
|
||||
// Arrange
|
||||
_userService.TwoFactorProviderIsEnabledAsync(
|
||||
providerType, user).Returns(true);
|
||||
|
||||
_userManager.TWO_FACTOR_ENABLED = true;
|
||||
_userManager.TWO_FACTOR_TOKEN_VERIFIED = true;
|
||||
|
||||
// Act
|
||||
var result = await _sut.VerifyTwoFactor(user, null, providerType, token);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(TwoFactorProviderType.Duo)]
|
||||
[BitAutoData(TwoFactorProviderType.WebAuthn)]
|
||||
[BitAutoData(TwoFactorProviderType.Email)]
|
||||
[BitAutoData(TwoFactorProviderType.YubiKey)]
|
||||
[BitAutoData(TwoFactorProviderType.Remember)]
|
||||
public async void VerifyTwoFactorAsync_Individual_InvalidToken_ReturnsFalse(
|
||||
TwoFactorProviderType providerType,
|
||||
User user,
|
||||
string token)
|
||||
{
|
||||
// Arrange
|
||||
_userService.TwoFactorProviderIsEnabledAsync(
|
||||
providerType, user).Returns(true);
|
||||
|
||||
_userManager.TWO_FACTOR_ENABLED = true;
|
||||
_userManager.TWO_FACTOR_TOKEN_VERIFIED = false;
|
||||
|
||||
// Act
|
||||
var result = await _sut.VerifyTwoFactor(user, null, providerType, token);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(TwoFactorProviderType.OrganizationDuo)]
|
||||
public async void VerifyTwoFactorAsync_Organization_ValidToken_ReturnsTrue(
|
||||
TwoFactorProviderType providerType,
|
||||
User user,
|
||||
Organization organization,
|
||||
string token)
|
||||
{
|
||||
// Arrange
|
||||
_organizationDuoWebTokenProvider.ValidateAsync(
|
||||
token, organization, user).Returns(true);
|
||||
|
||||
_userManager.TWO_FACTOR_ENABLED = true;
|
||||
_userManager.TWO_FACTOR_TOKEN_VERIFIED = true;
|
||||
|
||||
organization.Use2fa = true;
|
||||
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();
|
||||
organization.Enabled = true;
|
||||
|
||||
// Act
|
||||
var result = await _sut.VerifyTwoFactor(
|
||||
user, organization, providerType, token);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(TwoFactorProviderType.Duo)]
|
||||
[BitAutoData(TwoFactorProviderType.OrganizationDuo)]
|
||||
public async void VerifyTwoFactorAsync_TemporaryDuoService_ValidToken_ReturnsTrue(
|
||||
TwoFactorProviderType providerType,
|
||||
User user,
|
||||
Organization organization,
|
||||
string token)
|
||||
{
|
||||
// Arrange
|
||||
_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect).Returns(true);
|
||||
_userService.TwoFactorProviderIsEnabledAsync(providerType, user).Returns(true);
|
||||
_temporaryDuoWebV4SDKService.ValidateAsync(
|
||||
token, Arg.Any<TwoFactorProvider>(), user).Returns(true);
|
||||
|
||||
user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType);
|
||||
organization.Use2fa = true;
|
||||
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();
|
||||
organization.Enabled = true;
|
||||
|
||||
_userManager.TWO_FACTOR_ENABLED = true;
|
||||
_userManager.TWO_FACTOR_TOKEN = token;
|
||||
_userManager.TWO_FACTOR_TOKEN_VERIFIED = true;
|
||||
|
||||
// Act
|
||||
var result = await _sut.VerifyTwoFactor(
|
||||
user, organization, providerType, token);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(TwoFactorProviderType.Duo)]
|
||||
[BitAutoData(TwoFactorProviderType.OrganizationDuo)]
|
||||
public async void VerifyTwoFactorAsync_TemporaryDuoService_InvalidToken_ReturnsFalse(
|
||||
TwoFactorProviderType providerType,
|
||||
User user,
|
||||
Organization organization,
|
||||
string token)
|
||||
{
|
||||
// Arrange
|
||||
_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect).Returns(true);
|
||||
_userService.TwoFactorProviderIsEnabledAsync(providerType, user).Returns(true);
|
||||
_temporaryDuoWebV4SDKService.ValidateAsync(
|
||||
token, Arg.Any<TwoFactorProvider>(), user).Returns(true);
|
||||
|
||||
user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType);
|
||||
organization.Use2fa = true;
|
||||
organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson();
|
||||
organization.Enabled = true;
|
||||
|
||||
_userManager.TWO_FACTOR_ENABLED = true;
|
||||
_userManager.TWO_FACTOR_TOKEN = token;
|
||||
_userManager.TWO_FACTOR_TOKEN_VERIFIED = false;
|
||||
|
||||
// Act
|
||||
var result = await _sut.VerifyTwoFactor(
|
||||
user, organization, providerType, token);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
private static UserManagerTestWrapper<User> SubstituteUserManager()
|
||||
{
|
||||
return new UserManagerTestWrapper<User>(
|
||||
Substitute.For<IUserTwoFactorStore<User>>(),
|
||||
Substitute.For<IOptions<IdentityOptions>>(),
|
||||
Substitute.For<IPasswordHasher<User>>(),
|
||||
Enumerable.Empty<IUserValidator<User>>(),
|
||||
Enumerable.Empty<IPasswordValidator<User>>(),
|
||||
Substitute.For<ILookupNormalizer>(),
|
||||
Substitute.For<IdentityErrorDescriber>(),
|
||||
Substitute.For<IServiceProvider>(),
|
||||
Substitute.For<ILogger<UserManager<User>>>());
|
||||
}
|
||||
|
||||
private static string GetTwoFactorOrganizationDuoProviderJson(bool enabled = true)
|
||||
{
|
||||
return
|
||||
"{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}";
|
||||
}
|
||||
|
||||
private static string GetTwoFactorOrganizationNotEnabledDuoProviderJson(bool enabled = true)
|
||||
{
|
||||
return
|
||||
"{\"6\":{\"Enabled\":false,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}";
|
||||
}
|
||||
|
||||
private static string GetTwoFactorIndividualProviderJson(TwoFactorProviderType providerType)
|
||||
{
|
||||
return providerType switch
|
||||
{
|
||||
TwoFactorProviderType.Duo => "{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}",
|
||||
TwoFactorProviderType.Email => "{\"1\":{\"Enabled\":true,\"MetaData\":{\"Email\":\"user@test.dev\"}}}",
|
||||
TwoFactorProviderType.WebAuthn => "{\"7\":{\"Enabled\":true,\"MetaData\":{\"Key1\":{\"Name\":\"key1\",\"Descriptor\":{\"Type\":0,\"Id\":\"keyId\",\"Transports\":null},\"PublicKey\":\"key\",\"UserHandle\":\"handle\",\"SignatureCounter\":0,\"CredType\":\"none\",\"RegDate\":\"2022-01-01T00:00:00Z\",\"AaGuid\":\"00000000-0000-0000-0000-000000000000\",\"Migrated\":false}}}}",
|
||||
TwoFactorProviderType.YubiKey => "{\"3\":{\"Enabled\":true,\"MetaData\":{\"Id\":\"yubikeyId\",\"Nfc\":true}}}",
|
||||
TwoFactorProviderType.OrganizationDuo => "{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}",
|
||||
_ => "{}",
|
||||
};
|
||||
}
|
||||
|
||||
private static string GetTwoFactorIndividualNotEnabledProviderJson(TwoFactorProviderType providerType)
|
||||
{
|
||||
return providerType switch
|
||||
{
|
||||
TwoFactorProviderType.Duo => "{\"2\":{\"Enabled\":false,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}",
|
||||
TwoFactorProviderType.Email => "{\"1\":{\"Enabled\":false,\"MetaData\":{\"Email\":\"user@test.dev\"}}}",
|
||||
TwoFactorProviderType.WebAuthn => "{\"7\":{\"Enabled\":false,\"MetaData\":{\"Key1\":{\"Name\":\"key1\",\"Descriptor\":{\"Type\":0,\"Id\":\"keyId\",\"Transports\":null},\"PublicKey\":\"key\",\"UserHandle\":\"handle\",\"SignatureCounter\":0,\"CredType\":\"none\",\"RegDate\":\"2022-01-01T00:00:00Z\",\"AaGuid\":\"00000000-0000-0000-0000-000000000000\",\"Migrated\":false}}}}",
|
||||
TwoFactorProviderType.YubiKey => "{\"3\":{\"Enabled\":false,\"MetaData\":{\"Id\":\"yubikeyId\",\"Nfc\":true}}}",
|
||||
TwoFactorProviderType.OrganizationDuo => "{\"6\":{\"Enabled\":false,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}",
|
||||
_ => "{}",
|
||||
};
|
||||
}
|
||||
}
|
@ -1,16 +1,13 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Bit.Identity.IdentityServer.RequestValidators;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@ -54,38 +51,30 @@ IBaseRequestValidatorTestWrapper
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IDeviceValidator deviceValidator,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IMailService mailService,
|
||||
ILogger logger,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
IUserRepository userRepository,
|
||||
IPolicyService policyService,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) :
|
||||
base(
|
||||
base(
|
||||
userManager,
|
||||
userService,
|
||||
eventService,
|
||||
deviceValidator,
|
||||
organizationDuoWebTokenProvider,
|
||||
duoWebV4SDKService,
|
||||
organizationRepository,
|
||||
twoFactorAuthenticationValidator,
|
||||
organizationUserRepository,
|
||||
applicationCacheService,
|
||||
mailService,
|
||||
logger,
|
||||
currentContext,
|
||||
globalSettings,
|
||||
userRepository,
|
||||
policyService,
|
||||
tokenDataFactory,
|
||||
featureService,
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder)
|
||||
@ -98,13 +87,6 @@ IBaseRequestValidatorTestWrapper
|
||||
await ValidateAsync(context, context.ValidatedTokenRequest, context.CustomValidatorRequestContext);
|
||||
}
|
||||
|
||||
public async Task<Tuple<bool, Organization>> TestRequiresTwoFactorAsync(
|
||||
User user,
|
||||
ValidatedTokenRequest context)
|
||||
{
|
||||
return await RequiresTwoFactorAsync(user, context);
|
||||
}
|
||||
|
||||
protected override ClaimsPrincipal GetSubject(
|
||||
BaseRequestValidationContextFake context)
|
||||
{
|
||||
|
96
test/Identity.Test/Wrappers/UserManagerTestWrapper.cs
Normal file
96
test/Identity.Test/Wrappers/UserManagerTestWrapper.cs
Normal file
@ -0,0 +1,96 @@
|
||||
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Bit.Identity.Test.Wrappers;
|
||||
|
||||
public class UserManagerTestWrapper<TUser> : UserManager<TUser> where TUser : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Modify this value to mock the responses from UserManager.GetTwoFactorEnabledAsync()
|
||||
/// </summary>
|
||||
public bool TWO_FACTOR_ENABLED { get; set; } = false;
|
||||
/// <summary>
|
||||
/// Modify this value to mock the responses from UserManager.GetValidTwoFactorProvidersAsync()
|
||||
/// </summary>
|
||||
public IList<string> TWO_FACTOR_PROVIDERS { get; set; } = [];
|
||||
/// <summary>
|
||||
/// Modify this value to mock the responses from UserManager.GenerateTwoFactorTokenAsync()
|
||||
/// </summary>
|
||||
public string TWO_FACTOR_TOKEN { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Modify this value to mock the responses from UserManager.VerifyTwoFactorTokenAsync()
|
||||
/// </summary>
|
||||
public bool TWO_FACTOR_TOKEN_VERIFIED { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Modify this value to mock the responses from UserManager.SupportsUserTwoFactor
|
||||
/// </summary>
|
||||
public bool SUPPORTS_TWO_FACTOR { get; set; } = false;
|
||||
|
||||
public override bool SupportsUserTwoFactor
|
||||
{
|
||||
get
|
||||
{
|
||||
return SUPPORTS_TWO_FACTOR;
|
||||
}
|
||||
}
|
||||
|
||||
public UserManagerTestWrapper(
|
||||
IUserStore<TUser> store,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
IPasswordHasher<TUser> passwordHasher,
|
||||
IEnumerable<IUserValidator<TUser>> userValidators,
|
||||
IEnumerable<IPasswordValidator<TUser>> passwordValidators,
|
||||
ILookupNormalizer keyNormalizer,
|
||||
IdentityErrorDescriber errors,
|
||||
IServiceProvider services,
|
||||
ILogger<UserManager<TUser>> logger)
|
||||
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators,
|
||||
keyNormalizer, errors, services, logger)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// return class variable TWO_FACTOR_ENABLED
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
public override async Task<bool> GetTwoFactorEnabledAsync(TUser user)
|
||||
{
|
||||
return TWO_FACTOR_ENABLED;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// return class variable TWO_FACTOR_PROVIDERS
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <returns></returns>
|
||||
public override async Task<IList<string>> GetValidTwoFactorProvidersAsync(TUser user)
|
||||
{
|
||||
return TWO_FACTOR_PROVIDERS;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// return class variable TWO_FACTOR_TOKEN
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="tokenProvider"></param>
|
||||
/// <returns></returns>
|
||||
public override async Task<string> GenerateTwoFactorTokenAsync(TUser user, string tokenProvider)
|
||||
{
|
||||
return TWO_FACTOR_TOKEN;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// return class variable TWO_FACTOR_TOKEN_VERIFIED
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="tokenProvider"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
public override async Task<bool> VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token)
|
||||
{
|
||||
return TWO_FACTOR_TOKEN_VERIFIED;
|
||||
}
|
||||
}
|
@ -57,6 +57,16 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows you to add your own services to the application as required.
|
||||
/// </summary>
|
||||
/// <param name="configure">The service collection you want added to the test service collection.</param>
|
||||
/// <remarks>This needs to be ran BEFORE making any calls through the factory to take effect.</remarks>
|
||||
public void ConfigureServices(Action<IServiceCollection> configure)
|
||||
{
|
||||
_configureTestServices.Add(configure);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add your own configuration provider to the application.
|
||||
/// </summary>
|
||||
|
Loading…
Reference in New Issue
Block a user