Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Utilities.v2.Validation;
Expand All @@ -11,7 +12,8 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AcceptMem
public class AcceptOrganizationMembershipValidator(
IPolicyRequirementQuery policyRequirementQuery,
IAutomaticUserConfirmationPolicyEnforcementHandler automaticUserConfirmationPolicyEnforcementHandler,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IPolicyQuery policyQuery)
: IAcceptOrganizationMembershipValidator
{
public async Task<ValidationResult<AcceptOrganizationMembershipValidationResult>> ValidateAsync(
Expand Down Expand Up @@ -54,9 +56,12 @@ public async Task<ValidationResult<AcceptOrganizationMembershipValidationResult>
}
}

var autoConfirmPolicyStatus = await policyQuery.RunAsync(
request.OrganizationId, PolicyType.AutomaticUserConfirmation);

return Valid(new AcceptOrganizationMembershipValidationResult
{
AutoConfirmPolicyEnabled = autoConfirmRequirement.IsEnabled(request.OrganizationId)
AutoConfirmPolicyEnabled = autoConfirmPolicyStatus.Enabled
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,7 @@ public async Task<OrganizationUser> AcceptOrgUserAsync(OrganizationUser orgUser,
await _mailService.SendOrganizationAcceptedEmailAsync(organization, user.Email, adminEmails);
}

if (membershipValidationResult.AutoConfirmPolicyEnabled)
{
await _pushAutoConfirmNotificationCommand.PushAsync(user.Id, orgUser.OrganizationId);
}
await _pushAutoConfirmNotificationCommand.PushAsync(user.Id, orgUser.OrganizationId);

return orgUser;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,17 @@

public interface IPushAutoConfirmNotificationCommand
{
/// <summary>
/// Sends auto-confirm push notifications to all admins and custom users with ManageUsers permission
/// for the given organization, prompting them to confirm the newly accepted user.
/// </summary>
/// <remarks>
/// No notifications are sent if any of the following conditions are not met:
/// <list type="bullet">
/// <item>The organization has the <c>UseAutomaticUserConfirmation</c> ability enabled.</item>
/// <item>The organization has the <c>AutomaticUserConfirmation</c> policy enabled.</item>
/// <item>The user being confirmed has the <c>User</c> role (owners, admins, and custom are excluded).</item>
/// </list>
/// </remarks>
Task PushAsync(Guid userId, Guid organizationId);
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,58 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;

namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;

public class PushAutoConfirmNotificationCommand : IPushAutoConfirmNotificationCommand
{
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPushNotificationService _pushNotificationService;
private readonly IApplicationCacheService _applicationCacheService;
private readonly IPolicyQuery _policyQuery;

public PushAutoConfirmNotificationCommand(
IOrganizationUserRepository organizationUserRepository,
IPushNotificationService pushNotificationService)
IPushNotificationService pushNotificationService,
IApplicationCacheService applicationCacheService,
IPolicyQuery policyQuery)
{
_organizationUserRepository = organizationUserRepository;
_pushNotificationService = pushNotificationService;
_applicationCacheService = applicationCacheService;
_policyQuery = policyQuery;
}

public async Task PushAsync(Guid userId, Guid organizationId)
{
var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
if (orgAbility is not { UseAutomaticUserConfirmation: true })
{
return;
}

var policy = await _policyQuery.RunAsync(organizationId, PolicyType.AutomaticUserConfirmation);
if (!policy.Enabled)
{
return;
}

var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
if (organizationUser == null)
{
throw new Exception("Organization user not found");
}

if (organizationUser.Type != OrganizationUserType.User)
{
return;
}

var admins = await _organizationUserRepository.GetManyByMinimumRoleAsync(
organizationId,
OrganizationUserType.Admin);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using NSubstitute;
using Xunit;
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
using PolicyStatus = Bit.Core.AdminConsole.Models.Data.Organizations.Policies.PolicyStatus;

namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.AcceptMembership;

Expand Down Expand Up @@ -160,12 +161,10 @@ public async Task ValidateAsync_WhenAutoConfirmEnabled_SetsAutoConfirmPolicyEnab
{
SetupHappyPath(organizationId, user, sutProvider);

sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(user.Id)
.Returns(new AutomaticUserConfirmationPolicyRequirement(
[
new PolicyDetails { OrganizationId = organizationId }
]));
sutProvider.GetDependency<IPolicyQuery>()
.RunAsync(organizationId, PolicyType.AutomaticUserConfirmation)
.Returns(new PolicyStatus(organizationId, PolicyType.AutomaticUserConfirmation,
new Bit.Core.AdminConsole.Entities.Policy { Enabled = true }));

var request = new AcceptOrganizationMembershipValidationRequest
{
Expand All @@ -180,6 +179,45 @@ public async Task ValidateAsync_WhenAutoConfirmEnabled_SetsAutoConfirmPolicyEnab
Assert.True(result.Request.AutoConfirmPolicyEnabled);
}

/// <summary>
/// JIT SSO-provisioned org users have Status=Invited, UserId=set, Email=null, which satisfies neither
/// the UserId-matched nor the Email-matched branch in the user-keyed policy details query. Using the
/// org-keyed IPolicyQuery instead avoids the broken user→OrgUser matching entirely.
/// </summary>
[Theory, BitAutoData]
public async Task ValidateAsync_WhenAutoConfirmEnabledAndUserHasNoMatchingPolicyDetails_SetsAutoConfirmPolicyEnabledTrue(
Guid organizationId,
SutProvider<AcceptOrganizationMembershipValidator> sutProvider)
{
// Simulate a JIT SSO user whose org user record has Status=Invited/UserId=set/Email=null,
// so the user-keyed requirement returns empty (neither UserId nor Email branch matches).
var jitUser = new User { Id = Guid.NewGuid(), Email = null! };
SetupHappyPath(organizationId, jitUser, sutProvider);

// User-keyed requirement returns empty — the broken state for JIT SSO users.
sutProvider.GetDependency<IPolicyRequirementQuery>()
.GetAsync<AutomaticUserConfirmationPolicyRequirement>(jitUser.Id)
.Returns(new AutomaticUserConfirmationPolicyRequirement([]));

// But the org has auto-confirm enabled.
sutProvider.GetDependency<IPolicyQuery>()
.RunAsync(organizationId, PolicyType.AutomaticUserConfirmation)
.Returns(new PolicyStatus(organizationId, PolicyType.AutomaticUserConfirmation,
new Bit.Core.AdminConsole.Entities.Policy { Enabled = true }));

var request = new AcceptOrganizationMembershipValidationRequest
{
OrganizationId = organizationId,
User = jitUser,
AllOrganizationMemberships = [],
};

var result = await sutProvider.Sut.ValidateAsync(request);

Assert.True(result.IsValid);
Assert.True(result.Request.AutoConfirmPolicyEnabled);
}

[Theory, BitAutoData]
public async Task ValidateAsync_WhenAutoConfirmNotEnabled_SetsAutoConfirmPolicyEnabledFalse(
User user,
Expand Down Expand Up @@ -321,6 +359,10 @@ private static void SetupHappyPath(
Arg.Any<AutomaticUserConfirmationPolicyEnforcementRequest>(),
Arg.Any<AutomaticUserConfirmationPolicyRequirement>())
.Returns(callInfo => Valid(callInfo.Arg<AutomaticUserConfirmationPolicyEnforcementRequest>()));

sutProvider.GetDependency<IPolicyQuery>()
.RunAsync(organizationId, PolicyType.AutomaticUserConfirmation)
.Returns(new PolicyStatus(organizationId, PolicyType.AutomaticUserConfirmation));
}

[Theory, BitAutoData]
Expand Down
Loading
Loading