mirror of
https://github.com/bitwarden/server
synced 2026-01-06 02:23:51 +00:00
Merge remote-tracking branch 'origin/main' into xunit-v3-full-upgrade
This commit is contained in:
11
.github/workflows/build.yml
vendored
11
.github/workflows/build.yml
vendored
@@ -22,7 +22,7 @@ env:
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
build-artifacts:
|
||||
name: Build Docker images
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- lint
|
||||
outputs:
|
||||
@@ -49,7 +49,6 @@ jobs:
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 5
|
||||
matrix:
|
||||
include:
|
||||
- project_name: Admin
|
||||
@@ -292,7 +291,7 @@ jobs:
|
||||
|
||||
upload:
|
||||
name: Upload
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs: build-artifacts
|
||||
permissions:
|
||||
id-token: write
|
||||
@@ -410,7 +409,7 @@ jobs:
|
||||
|
||||
build-mssqlmigratorutility:
|
||||
name: Build MSSQL migrator utility
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- lint
|
||||
defaults:
|
||||
@@ -467,7 +466,7 @@ jobs:
|
||||
if: |
|
||||
github.event_name != 'pull_request'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- build-artifacts
|
||||
permissions:
|
||||
|
||||
@@ -57,7 +57,6 @@ services:
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: bw-mysql
|
||||
ports:
|
||||
- "3306:3306"
|
||||
command:
|
||||
@@ -88,7 +87,6 @@ services:
|
||||
|
||||
idp:
|
||||
image: kenchan0130/simplesamlphp:1.19.8
|
||||
container_name: idp
|
||||
ports:
|
||||
- "8090:8080"
|
||||
environment:
|
||||
@@ -102,7 +100,6 @@ services:
|
||||
|
||||
rabbitmq:
|
||||
image: rabbitmq:4.1.3-management
|
||||
container_name: rabbitmq
|
||||
ports:
|
||||
- "5672:5672"
|
||||
- "15672:15672"
|
||||
@@ -116,7 +113,6 @@ services:
|
||||
|
||||
reverse-proxy:
|
||||
image: nginx:alpine
|
||||
container_name: reverse-proxy
|
||||
volumes:
|
||||
- "./reverse-proxy.conf:/etc/nginx/conf.d/default.conf"
|
||||
ports:
|
||||
@@ -126,7 +122,6 @@ services:
|
||||
- proxy
|
||||
|
||||
service-bus:
|
||||
container_name: service-bus
|
||||
image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest
|
||||
pull_policy: always
|
||||
volumes:
|
||||
@@ -142,7 +137,6 @@ services:
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
container_name: bw-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Results;
|
||||
using Bit.Core.Models.Api;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
public abstract class BaseAdminConsoleController : Controller
|
||||
{
|
||||
protected static IResult Handle(CommandResult commandResult) =>
|
||||
commandResult.Match<IResult>(
|
||||
error => error switch
|
||||
{
|
||||
BadRequestError badRequest => TypedResults.BadRequest(new ErrorResponseModel(badRequest.Message)),
|
||||
NotFoundError notFound => TypedResults.NotFound(new ErrorResponseModel(notFound.Message)),
|
||||
InternalError internalError => TypedResults.Json(
|
||||
new ErrorResponseModel(internalError.Message),
|
||||
statusCode: StatusCodes.Status500InternalServerError),
|
||||
_ => TypedResults.Json(
|
||||
new ErrorResponseModel(error.Message),
|
||||
statusCode: StatusCodes.Status500InternalServerError
|
||||
)
|
||||
},
|
||||
_ => TypedResults.NoContent()
|
||||
);
|
||||
}
|
||||
@@ -11,8 +11,10 @@ using Bit.Api.Models.Response;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
@@ -20,6 +22,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
@@ -43,7 +46,7 @@ namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
[Route("organizations/{orgId}/users")]
|
||||
[Authorize("Application")]
|
||||
public class OrganizationUsersController : Controller
|
||||
public class OrganizationUsersController : BaseAdminConsoleController
|
||||
{
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
@@ -68,6 +71,7 @@ public class OrganizationUsersController : Controller
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
|
||||
private readonly IAutomaticallyConfirmOrganizationUserCommand _automaticallyConfirmOrganizationUserCommand;
|
||||
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
|
||||
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||
private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand;
|
||||
@@ -101,7 +105,8 @@ public class OrganizationUsersController : Controller
|
||||
IInitPendingOrganizationCommand initPendingOrganizationCommand,
|
||||
IRevokeOrganizationUserCommand revokeOrganizationUserCommand,
|
||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand,
|
||||
IAdminRecoverAccountCommand adminRecoverAccountCommand)
|
||||
IAdminRecoverAccountCommand adminRecoverAccountCommand,
|
||||
IAutomaticallyConfirmOrganizationUserCommand automaticallyConfirmOrganizationUserCommand)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@@ -126,6 +131,7 @@ public class OrganizationUsersController : Controller
|
||||
_featureService = featureService;
|
||||
_pricingClient = pricingClient;
|
||||
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
|
||||
_automaticallyConfirmOrganizationUserCommand = automaticallyConfirmOrganizationUserCommand;
|
||||
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
|
||||
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||
_initPendingOrganizationCommand = initPendingOrganizationCommand;
|
||||
@@ -738,6 +744,31 @@ public class OrganizationUsersController : Controller
|
||||
await BulkEnableSecretsManagerAsync(orgId, model);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/auto-confirm")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
[RequireFeature(FeatureFlagKeys.AutomaticConfirmUsers)]
|
||||
public async Task<IResult> AutomaticallyConfirmOrganizationUserAsync([FromRoute] Guid orgId,
|
||||
[FromRoute] Guid id,
|
||||
[FromBody] OrganizationUserConfirmRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
|
||||
if (userId is null || userId.Value == Guid.Empty)
|
||||
{
|
||||
return TypedResults.Unauthorized();
|
||||
}
|
||||
|
||||
return Handle(await _automaticallyConfirmOrganizationUserCommand.AutomaticallyConfirmOrganizationUserAsync(
|
||||
new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationId = orgId,
|
||||
OrganizationUserId = id,
|
||||
Key = model.Key,
|
||||
DefaultUserCollectionName = model.DefaultUserCollectionName,
|
||||
PerformedBy = new StandardUser(userId.Value, await _currentContext.OrganizationOwner(orgId)),
|
||||
}));
|
||||
}
|
||||
|
||||
private async Task RestoreOrRevokeUserAsync(
|
||||
Guid orgId,
|
||||
Guid id,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using Bit.Api.AdminConsole.Authorization;
|
||||
using Bit.Api.AdminConsole.Authorization.Requirements;
|
||||
using Bit.Api.Billing.Attributes;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Organizations.Queries;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace Bit.Api.Billing.Controllers.VNext;
|
||||
|
||||
[Authorize("Application")]
|
||||
[Route("organizations/{organizationId:guid}/billing/vnext/self-host")]
|
||||
[SelfHosted(SelfHostedOnly = true)]
|
||||
public class SelfHostedBillingController(
|
||||
IGetOrganizationMetadataQuery getOrganizationMetadataQuery) : BaseBillingController
|
||||
{
|
||||
[Authorize<MemberOrProviderRequirement>]
|
||||
[HttpGet("metadata")]
|
||||
[RequireFeature(FeatureFlagKeys.PM25379_UseNewOrganizationMetadataStructure)]
|
||||
[InjectOrganization]
|
||||
public async Task<IResult> GetMetadataAsync([BindNever] Organization organization)
|
||||
{
|
||||
var metadata = await getOrganizationMetadataQuery.Run(organization);
|
||||
|
||||
if (metadata == null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
return TypedResults.Ok(metadata);
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ public enum EventType : int
|
||||
OrganizationUser_RejectedAuthRequest = 1514,
|
||||
OrganizationUser_Deleted = 1515, // Both user and organization user data were deleted
|
||||
OrganizationUser_Left = 1516, // User voluntarily left the organization
|
||||
OrganizationUser_AutomaticallyConfirmed = 1517,
|
||||
|
||||
Organization_Updated = 1600,
|
||||
Organization_PurgedVault = 1601,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
|
||||
|
||||
public record AcceptedOrganizationUserToConfirm
|
||||
{
|
||||
public required Guid OrganizationUserId { get; init; }
|
||||
public required Guid UserId { get; init; }
|
||||
public required string Key { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OneOf.Types;
|
||||
using CommandResult = Bit.Core.AdminConsole.Utilities.v2.Results.CommandResult;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
|
||||
public class AutomaticallyConfirmOrganizationUserCommand(IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IAutomaticallyConfirmOrganizationUsersValidator validator,
|
||||
IEventService eventService,
|
||||
IMailService mailService,
|
||||
IUserRepository userRepository,
|
||||
IPushRegistrationService pushRegistrationService,
|
||||
IDeviceRepository deviceRepository,
|
||||
IPushNotificationService pushNotificationService,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
ICollectionRepository collectionRepository,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<AutomaticallyConfirmOrganizationUserCommand> logger) : IAutomaticallyConfirmOrganizationUserCommand
|
||||
{
|
||||
public async Task<CommandResult> AutomaticallyConfirmOrganizationUserAsync(AutomaticallyConfirmOrganizationUserRequest request)
|
||||
{
|
||||
var validatorRequest = await RetrieveDataAsync(request);
|
||||
|
||||
var validatedData = await validator.ValidateAsync(validatorRequest);
|
||||
|
||||
return await validatedData.Match<Task<CommandResult>>(
|
||||
error => Task.FromResult(new CommandResult(error)),
|
||||
async _ =>
|
||||
{
|
||||
var userToConfirm = new AcceptedOrganizationUserToConfirm
|
||||
{
|
||||
OrganizationUserId = validatedData.Request.OrganizationUser!.Id,
|
||||
UserId = validatedData.Request.OrganizationUser.UserId!.Value,
|
||||
Key = validatedData.Request.Key
|
||||
};
|
||||
|
||||
// This operation is idempotent. If false, the user is already confirmed and no additional side effects are required.
|
||||
if (!await organizationUserRepository.ConfirmOrganizationUserAsync(userToConfirm))
|
||||
{
|
||||
return new None();
|
||||
}
|
||||
|
||||
await CreateDefaultCollectionsAsync(validatedData.Request);
|
||||
|
||||
await Task.WhenAll(
|
||||
LogOrganizationUserConfirmedEventAsync(validatedData.Request),
|
||||
SendConfirmedOrganizationUserEmailAsync(validatedData.Request),
|
||||
SyncOrganizationKeysAsync(validatedData.Request)
|
||||
);
|
||||
|
||||
return new None();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async Task SyncOrganizationKeysAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
await DeleteDeviceRegistrationAsync(request);
|
||||
await PushSyncOrganizationKeysAsync(request);
|
||||
}
|
||||
|
||||
private async Task CreateDefaultCollectionsAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await ShouldCreateDefaultCollectionAsync(request))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await collectionRepository.CreateAsync(
|
||||
new Collection
|
||||
{
|
||||
OrganizationId = request.Organization!.Id,
|
||||
Name = request.DefaultUserCollectionName,
|
||||
Type = CollectionType.DefaultUserCollection
|
||||
},
|
||||
groups: null,
|
||||
[new CollectionAccessSelection
|
||||
{
|
||||
Id = request.OrganizationUser!.Id,
|
||||
Manage = true
|
||||
}]);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create default collection for user.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a default collection should be created for an organization user during the confirmation process.
|
||||
/// </summary>
|
||||
/// <param name="request">
|
||||
/// The validation request containing information about the user, organization, and collection settings.
|
||||
/// </param>
|
||||
/// <returns>The result is a boolean value indicating whether a default collection should be created.</returns>
|
||||
private async Task<bool> ShouldCreateDefaultCollectionAsync(AutomaticallyConfirmOrganizationUserValidationRequest request) =>
|
||||
!string.IsNullOrWhiteSpace(request.DefaultUserCollectionName)
|
||||
&& (await policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(request.OrganizationUser!.UserId!.Value))
|
||||
.RequiresDefaultCollectionOnConfirm(request.Organization!.Id);
|
||||
|
||||
private async Task PushSyncOrganizationKeysAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
await pushNotificationService.PushSyncOrgKeysAsync(request.OrganizationUser!.UserId!.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to push organization keys.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LogOrganizationUserConfirmedEventAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
await eventService.LogOrganizationUserEventAsync(request.OrganizationUser,
|
||||
EventType.OrganizationUser_AutomaticallyConfirmed,
|
||||
timeProvider.GetUtcNow().UtcDateTime);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to log OrganizationUser_AutomaticallyConfirmed event.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendConfirmedOrganizationUserEmailAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = await userRepository.GetByIdAsync(request.OrganizationUser!.UserId!.Value);
|
||||
|
||||
await mailService.SendOrganizationConfirmedEmailAsync(request.Organization!.Name,
|
||||
user!.Email,
|
||||
request.OrganizationUser.AccessSecretsManager);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to send OrganizationUserConfirmed.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteDeviceRegistrationAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var devices = (await deviceRepository.GetManyByUserIdAsync(request.OrganizationUser!.UserId!.Value))
|
||||
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
||||
.Select(d => d.Id.ToString());
|
||||
|
||||
await pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices, request.Organization!.Id.ToString());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to delete device registration.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AutomaticallyConfirmOrganizationUserValidationRequest> RetrieveDataAsync(
|
||||
AutomaticallyConfirmOrganizationUserRequest request)
|
||||
{
|
||||
return new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
OrganizationUserId = request.OrganizationUserId,
|
||||
OrganizationId = request.OrganizationId,
|
||||
Key = request.Key,
|
||||
DefaultUserCollectionName = request.DefaultUserCollectionName,
|
||||
PerformedBy = request.PerformedBy,
|
||||
OrganizationUser = await organizationUserRepository.GetByIdAsync(request.OrganizationUserId),
|
||||
Organization = await organizationRepository.GetByIdAsync(request.OrganizationId)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
|
||||
/// <summary>
|
||||
/// Automatically Confirm User Command Request
|
||||
/// </summary>
|
||||
public record AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
public required Guid OrganizationUserId { get; init; }
|
||||
public required Guid OrganizationId { get; init; }
|
||||
public required string Key { get; init; }
|
||||
public required string DefaultUserCollectionName { get; init; }
|
||||
public required IActingUser PerformedBy { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Automatically Confirm User Validation Request
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used to hold retrieved data and pass it to the validator
|
||||
/// </remarks>
|
||||
public record AutomaticallyConfirmOrganizationUserValidationRequest : AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
public OrganizationUser? OrganizationUser { get; set; }
|
||||
public Organization? Organization { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
|
||||
public class AutomaticallyConfirmOrganizationUsersValidator(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IPolicyRepository policyRepository) : IAutomaticallyConfirmOrganizationUsersValidator
|
||||
{
|
||||
public async Task<ValidationResult<AutomaticallyConfirmOrganizationUserValidationRequest>> ValidateAsync(
|
||||
AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
// User must exist
|
||||
if (request is { OrganizationUser: null } || request.OrganizationUser is { UserId: null })
|
||||
{
|
||||
return Invalid(request, new UserNotFoundError());
|
||||
}
|
||||
|
||||
// Organization must exist
|
||||
if (request is { Organization: null })
|
||||
{
|
||||
return Invalid(request, new OrganizationNotFound());
|
||||
}
|
||||
|
||||
// User must belong to the organization
|
||||
if (request.OrganizationUser.OrganizationId != request.Organization.Id)
|
||||
{
|
||||
return Invalid(request, new OrganizationUserIdIsInvalid());
|
||||
}
|
||||
|
||||
// User must be accepted
|
||||
if (request is { OrganizationUser.Status: not OrganizationUserStatusType.Accepted })
|
||||
{
|
||||
return Invalid(request, new UserIsNotAccepted());
|
||||
}
|
||||
|
||||
// User must be of type User
|
||||
if (request is { OrganizationUser.Type: not OrganizationUserType.User })
|
||||
{
|
||||
return Invalid(request, new UserIsNotUserType());
|
||||
}
|
||||
|
||||
if (!await OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(request))
|
||||
{
|
||||
return Invalid(request, new AutomaticallyConfirmUsersPolicyIsNotEnabled());
|
||||
}
|
||||
|
||||
if (!await OrganizationUserConformsToTwoFactorRequiredPolicyAsync(request))
|
||||
{
|
||||
return Invalid(request, new UserDoesNotHaveTwoFactorEnabled());
|
||||
}
|
||||
|
||||
if (await OrganizationUserConformsToSingleOrgPolicyAsync(request) is { } error)
|
||||
{
|
||||
return Invalid(request, error);
|
||||
}
|
||||
|
||||
return Valid(request);
|
||||
}
|
||||
|
||||
private async Task<bool> OrganizationHasAutomaticallyConfirmUsersPolicyEnabledAsync(
|
||||
AutomaticallyConfirmOrganizationUserValidationRequest request) =>
|
||||
await policyRepository.GetByOrganizationIdTypeAsync(request.OrganizationId,
|
||||
PolicyType.AutomaticUserConfirmation) is { Enabled: true }
|
||||
&& request.Organization is { UseAutomaticUserConfirmation: true };
|
||||
|
||||
private async Task<bool> OrganizationUserConformsToTwoFactorRequiredPolicyAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
if ((await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync([request.OrganizationUser!.UserId!.Value]))
|
||||
.Any(x => x.userId == request.OrganizationUser.UserId && x.twoFactorIsEnabled))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return !(await policyRequirementQuery.GetAsync<RequireTwoFactorPolicyRequirement>(request.OrganizationUser.UserId!.Value))
|
||||
.IsTwoFactorRequiredForOrganization(request.Organization!.Id);
|
||||
}
|
||||
|
||||
private async Task<Error?> OrganizationUserConformsToSingleOrgPolicyAsync(
|
||||
AutomaticallyConfirmOrganizationUserValidationRequest request)
|
||||
{
|
||||
var allOrganizationUsersForUser = await organizationUserRepository
|
||||
.GetManyByUserAsync(request.OrganizationUser!.UserId!.Value);
|
||||
|
||||
if (allOrganizationUsersForUser.Count == 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var policyRequirement = await policyRequirementQuery
|
||||
.GetAsync<SingleOrganizationPolicyRequirement>(request.OrganizationUser!.UserId!.Value);
|
||||
|
||||
if (policyRequirement.IsSingleOrgEnabledForThisOrganization(request.Organization!.Id))
|
||||
{
|
||||
return new OrganizationEnforcesSingleOrgPolicy();
|
||||
}
|
||||
|
||||
if (policyRequirement.IsSingleOrgEnabledForOrganizationsOtherThan(request.Organization.Id))
|
||||
{
|
||||
return new OtherOrganizationEnforcesSingleOrgPolicy();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
|
||||
public record OrganizationNotFound() : NotFoundError("Invalid organization");
|
||||
public record FailedToWriteToEventLog() : InternalError("Failed to write to event log");
|
||||
public record UserIsNotUserType() : BadRequestError("Only organization users with the User role can be automatically confirmed");
|
||||
public record UserIsNotAccepted() : BadRequestError("Cannot confirm user that has not accepted the invitation.");
|
||||
public record OrganizationUserIdIsInvalid() : BadRequestError("Invalid organization user id.");
|
||||
public record UserDoesNotHaveTwoFactorEnabled() : BadRequestError("User does not have two-step login enabled.");
|
||||
public record OrganizationEnforcesSingleOrgPolicy() : BadRequestError("Cannot confirm this member to the organization until they leave or remove all other organizations");
|
||||
public record OtherOrganizationEnforcesSingleOrgPolicy() : BadRequestError("Cannot confirm this member to the organization because they are in another organization which forbids it.");
|
||||
public record AutomaticallyConfirmUsersPolicyIsNotEnabled() : BadRequestError("Cannot confirm this member because the Automatically Confirm Users policy is not enabled.");
|
||||
@@ -0,0 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
|
||||
public interface IAutomaticallyConfirmOrganizationUsersValidator
|
||||
{
|
||||
Task<ValidationResult<AutomaticallyConfirmOrganizationUserValidationRequest>> ValidateAsync(
|
||||
AutomaticallyConfirmOrganizationUserValidationRequest request);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Results;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using static Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount.ValidationResultHelpers;
|
||||
using static Bit.Core.AdminConsole.Utilities.v2.Validation.ValidationResultHelpers;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
|
||||
/// <summary>
|
||||
/// A strongly typed error containing a reason that an action failed.
|
||||
/// This is used for business logic validation and other expected errors, not exceptions.
|
||||
/// </summary>
|
||||
public abstract record Error(string Message);
|
||||
/// <summary>
|
||||
/// An <see cref="Error"/> type that maps to a NotFoundResult at the api layer.
|
||||
/// </summary>
|
||||
/// <param name="Message"></param>
|
||||
public abstract record NotFoundError(string Message) : Error(Message);
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
|
||||
public record UserNotFoundError() : NotFoundError("Invalid user.");
|
||||
public record UserNotClaimedError() : Error("Member is not claimed by the organization.");
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Results;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
|
||||
public interface IDeleteClaimedOrganizationUserAccountCommand
|
||||
{
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
|
||||
public interface IDeleteClaimedOrganizationUserAccountValidator
|
||||
{
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Results;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Command to automatically confirm an organization user.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The auto-confirm feature enables eligible client apps to confirm OrganizationUsers
|
||||
/// automatically via push notifications, eliminating the need for manual administrator
|
||||
/// intervention. Client apps receive a push notification, perform the required key exchange,
|
||||
/// and submit an auto-confirm request to the server. This command processes those
|
||||
/// client-initiated requests and should only be used in that specific context.
|
||||
/// </remarks>
|
||||
public interface IAutomaticallyConfirmOrganizationUserCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Automatically confirms the organization user based on the provided request data.
|
||||
/// </summary>
|
||||
/// <param name="request">The request containing necessary information to confirm the organization user.</param>
|
||||
/// <remarks>
|
||||
/// This action has side effects. The side effects are
|
||||
/// <ul>
|
||||
/// <li>Creating an event log entry.</li>
|
||||
/// <li>Syncing organization keys with the user.</li>
|
||||
/// <li>Deleting any registered user devices for the organization.</li>
|
||||
/// <li>Sending an email to the confirmed user.</li>
|
||||
/// <li>Creating the default collection if applicable.</li>
|
||||
/// </ul>
|
||||
///
|
||||
/// Each of these actions is performed independently of each other and not guaranteed to be performed in any order.
|
||||
/// Errors will be reported back for the actions that failed in a consolidated error message.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// The result of the command. If there was an error, the result will contain a typed error describing the problem
|
||||
/// that occurred.
|
||||
/// </returns>
|
||||
Task<CommandResult> AutomaticallyConfirmOrganizationUserAsync(AutomaticallyConfirmOrganizationUserRequest request);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
public class SingleOrganizationPolicyRequirement(IEnumerable<PolicyDetails> policyDetails) : IPolicyRequirement
|
||||
{
|
||||
public bool IsSingleOrgEnabledForThisOrganization(Guid organizationId) =>
|
||||
policyDetails.Any(p => p.OrganizationId == organizationId);
|
||||
|
||||
public bool IsSingleOrgEnabledForOrganizationsOtherThan(Guid organizationId) =>
|
||||
policyDetails.Any(p => p.OrganizationId != organizationId);
|
||||
}
|
||||
|
||||
public class SingleOrganizationPolicyRequirementFactory : BasePolicyRequirementFactory<SingleOrganizationPolicyRequirement>
|
||||
{
|
||||
public override PolicyType PolicyType => PolicyType.SingleOrg;
|
||||
|
||||
public override SingleOrganizationPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails) =>
|
||||
new(policyDetails);
|
||||
}
|
||||
@@ -65,5 +65,6 @@ public static class PolicyServiceCollectionExtensions
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireSsoPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireTwoFactorPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, MasterPasswordPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SingleOrganizationPolicyRequirementFactory>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
@@ -29,8 +27,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator, IPolicyValidationEvent
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
|
||||
private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand;
|
||||
|
||||
@@ -40,8 +36,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator, IPolicyValidationEvent
|
||||
IOrganizationRepository organizationRepository,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
|
||||
IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand)
|
||||
{
|
||||
@@ -50,8 +44,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator, IPolicyValidationEvent
|
||||
_organizationRepository = organizationRepository;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_currentContext = currentContext;
|
||||
_featureService = featureService;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
_revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -93,7 +94,7 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
||||
///
|
||||
/// This is an idempotent operation.
|
||||
/// </summary>
|
||||
/// <param name="organizationUser">Accepted OrganizationUser to confirm</param>
|
||||
/// <param name="organizationUserToConfirm">Accepted OrganizationUser to confirm</param>
|
||||
/// <returns>True, if the user was updated. False, if not performed.</returns>
|
||||
Task<bool> ConfirmOrganizationUserAsync(OrganizationUser organizationUser);
|
||||
Task<bool> ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm);
|
||||
}
|
||||
|
||||
15
src/Core/AdminConsole/Utilities/v2/Errors.cs
Normal file
15
src/Core/AdminConsole/Utilities/v2/Errors.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Bit.Core.AdminConsole.Utilities.v2;
|
||||
|
||||
/// <summary>
|
||||
/// A strongly typed error containing a reason that an action failed.
|
||||
/// This is used for business logic validation and other expected errors, not exceptions.
|
||||
/// </summary>
|
||||
public abstract record Error(string Message);
|
||||
/// <summary>
|
||||
/// An <see cref="Error"/> type that maps to a NotFoundResult at the api layer.
|
||||
/// </summary>
|
||||
/// <param name="Message"></param>
|
||||
public abstract record NotFoundError(string Message) : Error(Message);
|
||||
|
||||
public abstract record BadRequestError(string Message) : Error(Message);
|
||||
public abstract record InternalError(string Message) : Error(Message);
|
||||
@@ -1,7 +1,7 @@
|
||||
using OneOf;
|
||||
using OneOf.Types;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
namespace Bit.Core.AdminConsole.Utilities.v2.Results;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of a command.
|
||||
@@ -39,4 +39,3 @@ public record BulkCommandResult<T>(Guid Id, CommandResult<T> Result);
|
||||
/// A wrapper for <see cref="CommandResult"/> with an ID, to identify the result in bulk operations.
|
||||
/// </summary>
|
||||
public record BulkCommandResult(Guid Id, CommandResult Result);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using OneOf;
|
||||
using OneOf.Types;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
namespace Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the result of validating a request.
|
||||
@@ -14,6 +14,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
@@ -135,6 +136,8 @@ public static class OrganizationServiceCollectionExtensions
|
||||
services.AddScoped<IUpdateOrganizationUserGroupsCommand, UpdateOrganizationUserGroupsCommand>();
|
||||
services.AddScoped<IConfirmOrganizationUserCommand, ConfirmOrganizationUserCommand>();
|
||||
services.AddScoped<IAdminRecoverAccountCommand, AdminRecoverAccountCommand>();
|
||||
services.AddScoped<IAutomaticallyConfirmOrganizationUserCommand, AutomaticallyConfirmOrganizationUserCommand>();
|
||||
services.AddScoped<IAutomaticallyConfirmOrganizationUsersValidator, AutomaticallyConfirmOrganizationUsersValidator>();
|
||||
|
||||
services.AddScoped<IDeleteClaimedOrganizationUserAccountCommand, DeleteClaimedOrganizationUserAccountCommand>();
|
||||
services.AddScoped<IDeleteClaimedOrganizationUserAccountValidator, DeleteClaimedOrganizationUserAccountValidator>();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.AdminConsole.Utilities.DebuggingInstruments;
|
||||
using Bit.Core.Entities;
|
||||
@@ -671,7 +672,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<bool> ConfirmOrganizationUserAsync(OrganizationUser organizationUser)
|
||||
public async Task<bool> ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm)
|
||||
{
|
||||
await using var connection = new SqlConnection(_marsConnectionString);
|
||||
|
||||
@@ -679,10 +680,10 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
$"[{Schema}].[OrganizationUser_ConfirmById]",
|
||||
new
|
||||
{
|
||||
organizationUser.Id,
|
||||
organizationUser.UserId,
|
||||
Id = organizationUserToConfirm.OrganizationUserId,
|
||||
UserId = organizationUserToConfirm.UserId,
|
||||
RevisionDate = DateTime.UtcNow.Date,
|
||||
Key = organizationUser.Key
|
||||
Key = organizationUserToConfirm.Key
|
||||
});
|
||||
|
||||
return rowCount > 0;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using AutoMapper;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@@ -943,23 +944,24 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ConfirmOrganizationUserAsync(Core.Entities.OrganizationUser organizationUser)
|
||||
public async Task<bool> ConfirmOrganizationUserAsync(AcceptedOrganizationUserToConfirm organizationUserToConfirm)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
await using var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var result = await dbContext.OrganizationUsers
|
||||
.Where(ou => ou.Id == organizationUser.Id && ou.Status == OrganizationUserStatusType.Accepted)
|
||||
.Where(ou => ou.Id == organizationUserToConfirm.OrganizationUserId
|
||||
&& ou.Status == OrganizationUserStatusType.Accepted)
|
||||
.ExecuteUpdateAsync(x => x
|
||||
.SetProperty(y => y.Status, OrganizationUserStatusType.Confirmed)
|
||||
.SetProperty(y => y.Key, organizationUser.Key));
|
||||
.SetProperty(y => y.Key, organizationUserToConfirm.Key));
|
||||
|
||||
if (result <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdAsync(organizationUser.Id);
|
||||
await dbContext.UserBumpAccountRevisionDateByOrganizationUserIdAsync(organizationUserToConfirm.OrganizationUserId);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ public class PolicyRepository : Repository<AdminConsoleEntities.Policy, Policy,
|
||||
UserId = u.Id
|
||||
}).ToListAsync();
|
||||
|
||||
// Combine results with provder lookup
|
||||
// Combine results with the provider lookup
|
||||
var allResults = acceptedUsers.Concat(invitedUsers)
|
||||
.Select(item => new OrganizationPolicyDetails
|
||||
{
|
||||
|
||||
@@ -60,6 +60,7 @@ CREATE TABLE [dbo].[Organization] (
|
||||
[UseAdminSponsoredFamilies] BIT NOT NULL CONSTRAINT [DF_Organization_UseAdminSponsoredFamilies] DEFAULT (0),
|
||||
[SyncSeats] BIT NOT NULL CONSTRAINT [DF_Organization_SyncSeats] DEFAULT (0),
|
||||
[UseAutomaticUserConfirmation] BIT NOT NULL CONSTRAINT [DF_Organization_UseAutomaticUserConfirmation] DEFAULT (0),
|
||||
[MaxStorageGbIncreased] SMALLINT NULL,
|
||||
CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||
);
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
[SecurityState] VARCHAR (MAX) NULL,
|
||||
[SecurityVersion] INT NULL,
|
||||
[SignedPublicKey] VARCHAR (MAX) NULL,
|
||||
[MaxStorageGbIncreased] SMALLINT NULL,
|
||||
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||
);
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ SELECT
|
||||
O.[UseSecretsManager],
|
||||
O.[Seats],
|
||||
O.[MaxCollections],
|
||||
O.[MaxStorageGb],
|
||||
COALESCE(O.[MaxStorageGbIncreased], O.[MaxStorageGb]) AS [MaxStorageGb],
|
||||
O.[Identifier],
|
||||
OU.[Key],
|
||||
OU.[ResetPasswordKey],
|
||||
|
||||
@@ -1,6 +1,66 @@
|
||||
CREATE VIEW [dbo].[OrganizationView]
|
||||
AS
|
||||
SELECT
|
||||
*
|
||||
[Id],
|
||||
[Identifier],
|
||||
[Name],
|
||||
[BusinessName],
|
||||
[BusinessAddress1],
|
||||
[BusinessAddress2],
|
||||
[BusinessAddress3],
|
||||
[BusinessCountry],
|
||||
[BusinessTaxNumber],
|
||||
[BillingEmail],
|
||||
[Plan],
|
||||
[PlanType],
|
||||
[Seats],
|
||||
[MaxCollections],
|
||||
[UsePolicies],
|
||||
[UseSso],
|
||||
[UseGroups],
|
||||
[UseDirectory],
|
||||
[UseEvents],
|
||||
[UseTotp],
|
||||
[Use2fa],
|
||||
[UseApi],
|
||||
[UseResetPassword],
|
||||
[SelfHost],
|
||||
[UsersGetPremium],
|
||||
[Storage],
|
||||
COALESCE([MaxStorageGbIncreased], [MaxStorageGb]) AS [MaxStorageGb],
|
||||
[Gateway],
|
||||
[GatewayCustomerId],
|
||||
[GatewaySubscriptionId],
|
||||
[ReferenceData],
|
||||
[Enabled],
|
||||
[LicenseKey],
|
||||
[PublicKey],
|
||||
[PrivateKey],
|
||||
[TwoFactorProviders],
|
||||
[ExpirationDate],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[OwnersNotifiedOfAutoscaling],
|
||||
[MaxAutoscaleSeats],
|
||||
[UseKeyConnector],
|
||||
[UseScim],
|
||||
[UseCustomPermissions],
|
||||
[UseSecretsManager],
|
||||
[Status],
|
||||
[UsePasswordManager],
|
||||
[SmSeats],
|
||||
[SmServiceAccounts],
|
||||
[MaxAutoscaleSmSeats],
|
||||
[MaxAutoscaleSmServiceAccounts],
|
||||
[SecretsManagerBeta],
|
||||
[LimitCollectionCreation],
|
||||
[LimitCollectionDeletion],
|
||||
[LimitItemDeletion],
|
||||
[AllowAdminAccessToAllCollectionItems],
|
||||
[UseRiskInsights],
|
||||
[UseOrganizationDomains],
|
||||
[UseAdminSponsoredFamilies],
|
||||
[SyncSeats],
|
||||
[UseAutomaticUserConfirmation]
|
||||
FROM
|
||||
[dbo].[Organization]
|
||||
|
||||
@@ -23,7 +23,7 @@ SELECT
|
||||
O.[UseCustomPermissions],
|
||||
O.[Seats],
|
||||
O.[MaxCollections],
|
||||
O.[MaxStorageGb],
|
||||
COALESCE(O.[MaxStorageGbIncreased], O.[MaxStorageGb]) AS [MaxStorageGb],
|
||||
O.[Identifier],
|
||||
PO.[Key],
|
||||
O.[PublicKey],
|
||||
|
||||
@@ -1,6 +1,51 @@
|
||||
CREATE VIEW [dbo].[UserView]
|
||||
AS
|
||||
SELECT
|
||||
*
|
||||
[Id],
|
||||
[Name],
|
||||
[Email],
|
||||
[EmailVerified],
|
||||
[MasterPassword],
|
||||
[MasterPasswordHint],
|
||||
[Culture],
|
||||
[SecurityStamp],
|
||||
[TwoFactorProviders],
|
||||
[TwoFactorRecoveryCode],
|
||||
[EquivalentDomains],
|
||||
[ExcludedGlobalEquivalentDomains],
|
||||
[AccountRevisionDate],
|
||||
[Key],
|
||||
[PublicKey],
|
||||
[PrivateKey],
|
||||
[Premium],
|
||||
[PremiumExpirationDate],
|
||||
[RenewalReminderDate],
|
||||
[Storage],
|
||||
COALESCE([MaxStorageGbIncreased], [MaxStorageGb]) AS [MaxStorageGb],
|
||||
[Gateway],
|
||||
[GatewayCustomerId],
|
||||
[GatewaySubscriptionId],
|
||||
[ReferenceData],
|
||||
[LicenseKey],
|
||||
[ApiKey],
|
||||
[Kdf],
|
||||
[KdfIterations],
|
||||
[KdfMemory],
|
||||
[KdfParallelism],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[ForcePasswordReset],
|
||||
[UsesKeyConnector],
|
||||
[FailedLoginCount],
|
||||
[LastFailedLoginDate],
|
||||
[AvatarColor],
|
||||
[LastPasswordChangeDate],
|
||||
[LastKdfChangeDate],
|
||||
[LastKeyRotationDate],
|
||||
[LastEmailChangeDate],
|
||||
[VerifyDevices],
|
||||
[SecurityState],
|
||||
[SecurityVersion],
|
||||
[SignedPublicKey]
|
||||
FROM
|
||||
[dbo].[User]
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
using System.Net;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Api.IntegrationTest.Helpers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
|
||||
|
||||
public class OrganizationUserControllerAutoConfirmTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
|
||||
{
|
||||
private const string _mockEncryptedString = "2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
|
||||
|
||||
private readonly HttpClient _client;
|
||||
private readonly ApiApplicationFactory _factory;
|
||||
private readonly LoginHelper _loginHelper;
|
||||
|
||||
private string _ownerEmail = null!;
|
||||
|
||||
public OrganizationUserControllerAutoConfirmTests(ApiApplicationFactory apiFactory)
|
||||
{
|
||||
_factory = apiFactory;
|
||||
_factory.SubstituteService<IFeatureService>(featureService =>
|
||||
{
|
||||
featureService
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
});
|
||||
_client = _factory.CreateClient();
|
||||
_loginHelper = new LoginHelper(_factory, _client);
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
_ownerEmail = $"org-owner-{Guid.NewGuid()}@example.com";
|
||||
await _factory.LoginWithNewAccount(_ownerEmail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoConfirm_WhenUserCannotManageOtherUsers_ThenShouldReturnForbidden()
|
||||
{
|
||||
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
|
||||
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
|
||||
|
||||
organization.UseAutomaticUserConfirmation = true;
|
||||
|
||||
await _factory.GetService<IOrganizationRepository>()
|
||||
.UpsertAsync(organization);
|
||||
|
||||
var testKey = $"test-key-{Guid.NewGuid()}";
|
||||
|
||||
var userToConfirmEmail = $"org-user-to-confirm-{Guid.NewGuid()}@example.com";
|
||||
await _factory.LoginWithNewAccount(userToConfirmEmail);
|
||||
|
||||
var (confirmingUserEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, organization.Id, OrganizationUserType.User);
|
||||
await _loginHelper.LoginAsync(confirmingUserEmail);
|
||||
|
||||
var organizationUser = await OrganizationTestHelpers.CreateUserAsync(
|
||||
_factory,
|
||||
organization.Id,
|
||||
userToConfirmEmail,
|
||||
OrganizationUserType.User,
|
||||
false,
|
||||
new Permissions { ManageUsers = false },
|
||||
OrganizationUserStatusType.Accepted);
|
||||
|
||||
var result = await _client.PostAsJsonAsync($"organizations/{organization.Id}/users/{organizationUser.Id}/auto-confirm",
|
||||
new OrganizationUserConfirmRequestModel
|
||||
{
|
||||
Key = testKey,
|
||||
DefaultUserCollectionName = _mockEncryptedString
|
||||
});
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, result.StatusCode);
|
||||
|
||||
await _factory.GetService<IOrganizationRepository>().DeleteAsync(organization);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoConfirm_WhenOwnerConfirmsValidUser_ThenShouldReturnNoContent()
|
||||
{
|
||||
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
|
||||
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
|
||||
|
||||
organization.UseAutomaticUserConfirmation = true;
|
||||
|
||||
await _factory.GetService<IOrganizationRepository>()
|
||||
.UpsertAsync(organization);
|
||||
|
||||
var testKey = $"test-key-{Guid.NewGuid()}";
|
||||
|
||||
await _factory.GetService<IPolicyRepository>().CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = PolicyType.AutomaticUserConfirmation,
|
||||
Enabled = true
|
||||
});
|
||||
|
||||
await _factory.GetService<IPolicyRepository>().CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = PolicyType.OrganizationDataOwnership,
|
||||
Enabled = true
|
||||
});
|
||||
|
||||
var userToConfirmEmail = $"org-user-to-confirm-{Guid.NewGuid()}@example.com";
|
||||
await _factory.LoginWithNewAccount(userToConfirmEmail);
|
||||
|
||||
await _loginHelper.LoginAsync(_ownerEmail);
|
||||
var organizationUser = await OrganizationTestHelpers.CreateUserAsync(
|
||||
_factory,
|
||||
organization.Id,
|
||||
userToConfirmEmail,
|
||||
OrganizationUserType.User,
|
||||
false,
|
||||
new Permissions(),
|
||||
OrganizationUserStatusType.Accepted);
|
||||
|
||||
var result = await _client.PostAsJsonAsync($"organizations/{organization.Id}/users/{organizationUser.Id}/auto-confirm",
|
||||
new OrganizationUserConfirmRequestModel
|
||||
{
|
||||
Key = testKey,
|
||||
DefaultUserCollectionName = _mockEncryptedString
|
||||
});
|
||||
|
||||
Assert.Equal(HttpStatusCode.NoContent, result.StatusCode);
|
||||
|
||||
var orgUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||
var confirmedUser = await orgUserRepository.GetByIdAsync(organizationUser.Id);
|
||||
Assert.NotNull(confirmedUser);
|
||||
Assert.Equal(OrganizationUserStatusType.Confirmed, confirmedUser.Status);
|
||||
Assert.Equal(testKey, confirmedUser.Key);
|
||||
|
||||
var collectionRepository = _factory.GetService<ICollectionRepository>();
|
||||
var collections = await collectionRepository.GetManyByUserIdAsync(organizationUser.UserId!.Value);
|
||||
|
||||
Assert.NotEmpty(collections);
|
||||
Assert.Single(collections.Where(c => c.Type == CollectionType.DefaultUserCollection));
|
||||
|
||||
await _factory.GetService<IOrganizationRepository>().DeleteAsync(organization);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AutoConfirm_WhenUserIsConfirmedMultipleTimes_ThenShouldSuccessAndOnlyConfirmOneUser()
|
||||
{
|
||||
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
|
||||
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
|
||||
|
||||
organization.UseAutomaticUserConfirmation = true;
|
||||
|
||||
await _factory.GetService<IOrganizationRepository>()
|
||||
.UpsertAsync(organization);
|
||||
|
||||
var testKey = $"test-key-{Guid.NewGuid()}";
|
||||
|
||||
var userToConfirmEmail = $"org-user-to-confirm-{Guid.NewGuid()}@example.com";
|
||||
await _factory.LoginWithNewAccount(userToConfirmEmail);
|
||||
|
||||
await _factory.GetService<IPolicyRepository>().CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = PolicyType.AutomaticUserConfirmation,
|
||||
Enabled = true
|
||||
});
|
||||
|
||||
await _factory.GetService<IPolicyRepository>().CreateAsync(new Policy
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Type = PolicyType.OrganizationDataOwnership,
|
||||
Enabled = true
|
||||
});
|
||||
|
||||
await _loginHelper.LoginAsync(_ownerEmail);
|
||||
|
||||
var organizationUser = await OrganizationTestHelpers.CreateUserAsync(
|
||||
_factory,
|
||||
organization.Id,
|
||||
userToConfirmEmail,
|
||||
OrganizationUserType.User,
|
||||
false,
|
||||
new Permissions(),
|
||||
OrganizationUserStatusType.Accepted);
|
||||
|
||||
var tenRequests = Enumerable.Range(0, 10)
|
||||
.Select(_ => _client.PostAsJsonAsync($"organizations/{organization.Id}/users/{organizationUser.Id}/auto-confirm",
|
||||
new OrganizationUserConfirmRequestModel
|
||||
{
|
||||
Key = testKey,
|
||||
DefaultUserCollectionName = _mockEncryptedString
|
||||
})).ToList();
|
||||
|
||||
var results = await Task.WhenAll(tenRequests);
|
||||
|
||||
Assert.Contains(results, r => r.StatusCode == HttpStatusCode.NoContent);
|
||||
|
||||
var orgUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||
var confirmedUser = await orgUserRepository.GetByIdAsync(organizationUser.Id);
|
||||
Assert.NotNull(confirmedUser);
|
||||
Assert.Equal(OrganizationUserStatusType.Confirmed, confirmedUser.Status);
|
||||
Assert.Equal(testKey, confirmedUser.Key);
|
||||
|
||||
var collections = await _factory.GetService<ICollectionRepository>()
|
||||
.GetManyByUserIdAsync(organizationUser.UserId!.Value);
|
||||
Assert.NotEmpty(collections);
|
||||
// validates user only received one default collection
|
||||
Assert.Single(collections.Where(c => c.Type == CollectionType.DefaultUserCollection));
|
||||
|
||||
await _factory.GetService<IOrganizationRepository>().DeleteAsync(organization);
|
||||
}
|
||||
|
||||
public Task DisposeAsync()
|
||||
{
|
||||
_client.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,12 @@ using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Results;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
@@ -33,9 +35,11 @@ using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NSubstitute;
|
||||
using OneOf.Types;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Controllers;
|
||||
@@ -476,7 +480,7 @@ public class OrganizationUsersControllerTests
|
||||
|
||||
var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);
|
||||
|
||||
Assert.IsType<NotFound>(result);
|
||||
Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.NotFound>(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -506,7 +510,7 @@ public class OrganizationUsersControllerTests
|
||||
|
||||
var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);
|
||||
|
||||
Assert.IsType<NotFound>(result);
|
||||
Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.NotFound>(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -521,7 +525,7 @@ public class OrganizationUsersControllerTests
|
||||
|
||||
var result = await sutProvider.Sut.PutResetPassword(orgId, orgUserId, model);
|
||||
|
||||
Assert.IsType<NotFound>(result);
|
||||
Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.NotFound>(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
@@ -594,4 +598,190 @@ public class OrganizationUsersControllerTests
|
||||
|
||||
Assert.IsType<BadRequest<ModelStateDictionary>>(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_UserIdNull_ReturnsUnauthorized(
|
||||
Guid orgId,
|
||||
Guid orgUserId,
|
||||
OrganizationUserConfirmRequestModel model,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns((Guid?)null);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<UnauthorizedHttpResult>(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_UserIdEmpty_ReturnsUnauthorized(
|
||||
Guid orgId,
|
||||
Guid orgUserId,
|
||||
OrganizationUserConfirmRequestModel model,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(Guid.Empty);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<UnauthorizedHttpResult>(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_Success_ReturnsOk(
|
||||
Guid orgId,
|
||||
Guid orgUserId,
|
||||
Guid userId,
|
||||
OrganizationUserConfirmRequestModel model,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(userId);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(orgId)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()
|
||||
.AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())
|
||||
.Returns(new CommandResult(new None()));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<NoContent>(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_NotFoundError_ReturnsNotFound(
|
||||
Guid orgId,
|
||||
Guid orgUserId,
|
||||
Guid userId,
|
||||
OrganizationUserConfirmRequestModel model,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(userId);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(orgId)
|
||||
.Returns(false);
|
||||
|
||||
var notFoundError = new OrganizationNotFound();
|
||||
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()
|
||||
.AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())
|
||||
.Returns(new CommandResult(notFoundError));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
|
||||
|
||||
// Assert
|
||||
var notFoundResult = Assert.IsType<NotFound<ErrorResponseModel>>(result);
|
||||
Assert.Equal(notFoundError.Message, notFoundResult.Value.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_BadRequestError_ReturnsBadRequest(
|
||||
Guid orgId,
|
||||
Guid orgUserId,
|
||||
Guid userId,
|
||||
OrganizationUserConfirmRequestModel model,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(userId);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(orgId)
|
||||
.Returns(true);
|
||||
|
||||
var badRequestError = new UserIsNotAccepted();
|
||||
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()
|
||||
.AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())
|
||||
.Returns(new CommandResult(badRequestError));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
|
||||
|
||||
// Assert
|
||||
var badRequestResult = Assert.IsType<BadRequest<ErrorResponseModel>>(result);
|
||||
Assert.Equal(badRequestError.Message, badRequestResult.Value.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_InternalError_ReturnsProblem(
|
||||
Guid orgId,
|
||||
Guid orgUserId,
|
||||
Guid userId,
|
||||
OrganizationUserConfirmRequestModel model,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.AutomaticConfirmUsers)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetProperUserId(Arg.Any<ClaimsPrincipal>())
|
||||
.Returns(userId);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(orgId)
|
||||
.Returns(true);
|
||||
|
||||
var internalError = new FailedToWriteToEventLog();
|
||||
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUserCommand>()
|
||||
.AutomaticallyConfirmOrganizationUserAsync(Arg.Any<AutomaticallyConfirmOrganizationUserRequest>())
|
||||
.Returns(new CommandResult(internalError));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(orgId, orgUserId, model);
|
||||
|
||||
// Assert
|
||||
var problemResult = Assert.IsType<JsonHttpResult<ErrorResponseModel>>(result);
|
||||
Assert.Equal(StatusCodes.Status500InternalServerError, problemResult.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Text.Json;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using AutoFixture;
|
||||
using AutoFixture.Kernel;
|
||||
using AutoFixture.Xunit2;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
@@ -20,6 +22,18 @@ public class OrganizationCustomization : ICustomization
|
||||
{
|
||||
public bool UseGroups { get; set; }
|
||||
public PlanType PlanType { get; set; }
|
||||
public bool UseAutomaticUserConfirmation { get; set; }
|
||||
|
||||
public OrganizationCustomization()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public OrganizationCustomization(bool useAutomaticUserConfirmation, PlanType planType)
|
||||
{
|
||||
UseAutomaticUserConfirmation = useAutomaticUserConfirmation;
|
||||
PlanType = planType;
|
||||
}
|
||||
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
@@ -37,7 +51,8 @@ public class OrganizationCustomization : ICustomization
|
||||
.With(o => o.UseGroups, UseGroups)
|
||||
.With(o => o.PlanType, PlanType)
|
||||
.With(o => o.Seats, seats)
|
||||
.With(o => o.SmSeats, smSeats));
|
||||
.With(o => o.SmSeats, smSeats)
|
||||
.With(o => o.UseAutomaticUserConfirmation, UseAutomaticUserConfirmation));
|
||||
|
||||
fixture.Customize<Collection>(composer =>
|
||||
composer
|
||||
@@ -277,3 +292,9 @@ internal class EphemeralDataProtectionAutoDataAttribute : CustomAutoDataAttribut
|
||||
public EphemeralDataProtectionAutoDataAttribute() : base(new SutProviderCustomization(), new EphemeralDataProtectionCustomization())
|
||||
{ }
|
||||
}
|
||||
|
||||
internal class OrganizationAttribute(bool useAutomaticUserConfirmation = false, PlanType planType = PlanType.Free) : CustomizeAttribute
|
||||
{
|
||||
public override ICustomization GetCustomization(ParameterInfo parameter) =>
|
||||
new OrganizationCustomization(useAutomaticUserConfirmation, planType);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,696 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUsers;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class AutomaticallyConfirmOrganizationUsersValidatorTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithNullOrganizationUser_ReturnsUserNotFoundError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
Organization organization)
|
||||
{
|
||||
// Arrange
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = null,
|
||||
OrganizationUserId = Guid.NewGuid(),
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<UserNotFoundError>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithNullUserId_ReturnsUserNotFoundError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = null;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<UserNotFoundError>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithNullOrganization_ReturnsOrganizationNotFoundError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = null,
|
||||
OrganizationId = organizationUser.OrganizationId,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<OrganizationNotFound>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithValidAcceptedUser_ReturnsValidResult(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true, planType: PlanType.EnterpriseAnnually)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal(request, result.Request);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithMismatchedOrganizationId_ReturnsOrganizationUserIdIsInvalidError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = Guid.NewGuid(); // Different from organization.Id
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<OrganizationUserIdIsInvalid>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserStatusType.Invited)]
|
||||
[BitAutoData(OrganizationUserStatusType.Revoked)]
|
||||
[BitAutoData(OrganizationUserStatusType.Confirmed)]
|
||||
public async Task ValidateAsync_WithNotAcceptedStatus_ReturnsUserIsNotAcceptedError(
|
||||
OrganizationUserStatusType statusType,
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||
Guid userId)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
organizationUser.Status = statusType;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<UserIsNotAccepted>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
[BitAutoData(OrganizationUserType.Custom)]
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
public async Task ValidateAsync_WithNonUserType_ReturnsUserIsNotUserTypeError(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
organizationUser.Type = userType;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<UserIsNotUserType>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserWithout2FA_And2FARequired_ReturnsError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
var twoFactorPolicyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
PolicyType = PolicyType.TwoFactorAuthentication
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, false)]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireTwoFactorPolicyRequirement>(userId)
|
||||
.Returns(new RequireTwoFactorPolicyRequirement([twoFactorPolicyDetails]));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<UserDoesNotHaveTwoFactorEnabled>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserWith2FA_ReturnsValidResult(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserWithout2FA_And2FANotRequired_ReturnsValidResult(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, false)]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireTwoFactorPolicyRequirement>(userId)
|
||||
.Returns(new RequireTwoFactorPolicyRequirement([])); // No 2FA policy
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserInMultipleOrgs_WithSingleOrgPolicyOnThisOrg_ReturnsError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
OrganizationUser otherOrgUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
var singleOrgPolicyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
PolicyType = PolicyType.SingleOrg
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser, otherOrgUser]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<SingleOrganizationPolicyRequirement>(userId)
|
||||
.Returns(new SingleOrganizationPolicyRequirement([singleOrgPolicyDetails]));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<OrganizationEnforcesSingleOrgPolicy>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserInMultipleOrgs_WithSingleOrgPolicyOnOtherOrg_ReturnsError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
OrganizationUser otherOrgUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
var otherOrgId = Guid.NewGuid(); // Different org
|
||||
var singleOrgPolicyDetails = new PolicyDetails
|
||||
{
|
||||
OrganizationId = otherOrgId,
|
||||
PolicyType = PolicyType.SingleOrg,
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser, otherOrgUser]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<SingleOrganizationPolicyRequirement>(userId)
|
||||
.Returns(new SingleOrganizationPolicyRequirement([singleOrgPolicyDetails]));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<OtherOrganizationEnforcesSingleOrgPolicy>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserInSingleOrg_ReturnsValidResult(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser]); // Single org
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_UserInMultipleOrgs_WithNoSingleOrgPolicy_ReturnsValidResult(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: true)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
OrganizationUser otherOrgUser,
|
||||
Guid userId,
|
||||
Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
autoConfirmPolicy.Type = PolicyType.AutomaticUserConfirmation;
|
||||
autoConfirmPolicy.Enabled = true;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser, otherOrgUser]);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<SingleOrganizationPolicyRequirement>(userId)
|
||||
.Returns(new SingleOrganizationPolicyRequirement([]));
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsValid);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithAutoConfirmPolicyDisabled_ReturnsAutoConfirmPolicyNotEnabledError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns((Policy)null);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<AutomaticallyConfirmUsersPolicyIsNotEnabled>(result.AsError);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task ValidateAsync_WithOrganizationUseAutomaticUserConfirmationDisabled_ReturnsAutoConfirmPolicyNotEnabledError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUsersValidator> sutProvider,
|
||||
[Organization(useAutomaticUserConfirmation: false)] Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
Guid userId,
|
||||
[Policy(PolicyType.AutomaticUserConfirmation)] Policy autoConfirmPolicy)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = userId;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = Substitute.For<IActingUser>(),
|
||||
DefaultUserCollectionName = "test-collection",
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
Organization = organization,
|
||||
OrganizationId = organization.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.AutomaticUserConfirmation)
|
||||
.Returns(autoConfirmPolicy);
|
||||
|
||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<Guid>>())
|
||||
.Returns([(userId, true)]);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyByUserAsync(userId)
|
||||
.Returns([organizationUser]);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.ValidateAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<AutomaticallyConfirmUsersPolicyIsNotEnabled>(result.AsError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,730 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUsers;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class AutomaticallyConfirmUsersCommandTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WithValidRequest_ConfirmsUserSuccessfully(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key));
|
||||
|
||||
await AssertSuccessfulOperationsAsync(sutProvider, organizationUser, organization, user, key);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WithInvalidUserOrgId_ReturnsOrganizationUserIdIsInvalidError(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = Guid.NewGuid(); // User belongs to another organization
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, false, new OrganizationUserIdIsInvalid());
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsError);
|
||||
Assert.IsType<OrganizationUserIdIsInvalid>(result.AsError);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.DidNotReceive()
|
||||
.ConfirmOrganizationUserAsync(Arg.Any<AcceptedOrganizationUserToConfirm>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WhenAlreadyConfirmed_ReturnsNoneSuccess(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
|
||||
// Return false to indicate the user is already confirmed
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(x =>
|
||||
x.OrganizationUserId == organizationUser.Id && x.Key == request.Key))
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.Received(1)
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(x =>
|
||||
x.OrganizationUserId == organizationUser.Id && x.Key == request.Key));
|
||||
|
||||
// Verify no side effects occurred
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.DidNotReceive()
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<DateTime?>());
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.DidNotReceive()
|
||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WithDefaultCollectionEnabled_CreatesDefaultCollection(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName, // Non-empty to trigger creation
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
SetupPolicyRequirementMock(sutProvider, user.Id, organization.Id, true); // Policy requires collection
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>().ConfirmOrganizationUserAsync(
|
||||
Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.Received(1)
|
||||
.CreateAsync(
|
||||
Arg.Is<Collection>(c =>
|
||||
c.OrganizationId == organization.Id &&
|
||||
c.Name == defaultCollectionName &&
|
||||
c.Type == CollectionType.DefaultUserCollection),
|
||||
Arg.Is<IEnumerable<CollectionAccessSelection>>(groups => groups == null),
|
||||
Arg.Is<IEnumerable<CollectionAccessSelection>>(access =>
|
||||
access.FirstOrDefault(x => x.Id == organizationUser.Id && x.Manage) != null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WithDefaultCollectionDisabled_DoesNotCreateCollection(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = string.Empty, // Empty, so the collection won't be created
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
SetupPolicyRequirementMock(sutProvider, user.Id, organization.Id, false); // Policy doesn't require
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
await sutProvider.GetDependency<ICollectionRepository>()
|
||||
.DidNotReceive()
|
||||
.CreateAsync(Arg.Any<Collection>(),
|
||||
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
|
||||
Arg.Any<IEnumerable<CollectionAccessSelection>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WhenCreateDefaultCollectionFails_LogsErrorButReturnsSuccess(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName, // Non-empty to trigger creation
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
SetupPolicyRequirementMock(sutProvider, user.Id, organization.Id, true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key)).Returns(true);
|
||||
|
||||
var collectionException = new Exception("Collection creation failed");
|
||||
sutProvider.GetDependency<ICollectionRepository>()
|
||||
.CreateAsync(Arg.Any<Collection>(),
|
||||
Arg.Any<IEnumerable<CollectionAccessSelection>>(),
|
||||
Arg.Any<IEnumerable<CollectionAccessSelection>>())
|
||||
.ThrowsAsync(collectionException);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert - side effects are fire-and-forget, so command returns success even if collection creation fails
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
sutProvider.GetDependency<ILogger<AutomaticallyConfirmOrganizationUserCommand>>()
|
||||
.Received(1)
|
||||
.Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => o.ToString()!.Contains("Failed to create default collection for user")),
|
||||
collectionException,
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WhenEventLogFails_LogsErrorButReturnsSuccess(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
var eventException = new Exception("Event logging failed");
|
||||
sutProvider.GetDependency<IEventService>()
|
||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(),
|
||||
EventType.OrganizationUser_AutomaticallyConfirmed,
|
||||
Arg.Any<DateTime?>())
|
||||
.ThrowsAsync(eventException);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert - side effects are fire-and-forget, so command returns success even if event log fails
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
sutProvider.GetDependency<ILogger<AutomaticallyConfirmOrganizationUserCommand>>()
|
||||
.Received(1)
|
||||
.Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => o.ToString()!.Contains("Failed to log OrganizationUser_AutomaticallyConfirmed event")),
|
||||
eventException,
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WhenSendEmailFails_LogsErrorButReturnsSuccess(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
var emailException = new Exception("Email sending failed");
|
||||
sutProvider.GetDependency<IMailService>()
|
||||
.SendOrganizationConfirmedEmailAsync(organization.Name, user.Email, organizationUser.AccessSecretsManager)
|
||||
.ThrowsAsync(emailException);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert - side effects are fire-and-forget, so command returns success even if email fails
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
sutProvider.GetDependency<ILogger<AutomaticallyConfirmOrganizationUserCommand>>()
|
||||
.Received(1)
|
||||
.Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => o.ToString()!.Contains("Failed to send OrganizationUserConfirmed")),
|
||||
emailException,
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WhenUserNotFoundForEmail_LogsErrorButReturnsSuccess(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
// Return null when retrieving user for email
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(user.Id)
|
||||
.Returns((User)null!);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert - side effects are fire-and-forget, so command returns success even if user not found for email
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WhenDeleteDeviceRegistrationFails_LogsErrorButReturnsSuccess(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName,
|
||||
Device device)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
device.UserId = user.Id;
|
||||
device.PushToken = "test-push-token";
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IDeviceRepository>()
|
||||
.GetManyByUserIdAsync(user.Id)
|
||||
.Returns(new List<Device> { device });
|
||||
|
||||
var deviceException = new Exception("Device registration deletion failed");
|
||||
sutProvider.GetDependency<IPushRegistrationService>()
|
||||
.DeleteUserRegistrationOrganizationAsync(Arg.Any<IEnumerable<string>>(), organization.Id.ToString())
|
||||
.ThrowsAsync(deviceException);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert - side effects are fire-and-forget, so command returns success even if device registration deletion fails
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
sutProvider.GetDependency<ILogger<AutomaticallyConfirmOrganizationUserCommand>>()
|
||||
.Received(1)
|
||||
.Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => o.ToString()!.Contains("Failed to delete device registration")),
|
||||
deviceException,
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WhenPushSyncOrgKeysFails_LogsErrorButReturnsSuccess(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
var pushException = new Exception("Push sync failed");
|
||||
sutProvider.GetDependency<IPushNotificationService>()
|
||||
.PushSyncOrgKeysAsync(user.Id)
|
||||
.ThrowsAsync(pushException);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert - side effects are fire-and-forget, so command returns success even if push sync fails
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
sutProvider.GetDependency<ILogger<AutomaticallyConfirmOrganizationUserCommand>>()
|
||||
.Received(1)
|
||||
.Log(
|
||||
LogLevel.Error,
|
||||
Arg.Any<EventId>(),
|
||||
Arg.Is<object>(o => o.ToString()!.Contains("Failed to push organization keys")),
|
||||
pushException,
|
||||
Arg.Any<Func<object, Exception?, string>>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task AutomaticallyConfirmOrganizationUserAsync_WithDevicesWithoutPushToken_FiltersCorrectly(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Organization organization,
|
||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser organizationUser,
|
||||
User user,
|
||||
Guid performingUserId,
|
||||
string key,
|
||||
string defaultCollectionName,
|
||||
Device deviceWithToken,
|
||||
Device deviceWithoutToken)
|
||||
{
|
||||
// Arrange
|
||||
organizationUser.UserId = user.Id;
|
||||
organizationUser.OrganizationId = organization.Id;
|
||||
deviceWithToken.UserId = user.Id;
|
||||
deviceWithToken.PushToken = "test-token";
|
||||
deviceWithoutToken.UserId = user.Id;
|
||||
deviceWithoutToken.PushToken = null;
|
||||
var request = new AutomaticallyConfirmOrganizationUserRequest
|
||||
{
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key,
|
||||
DefaultUserCollectionName = defaultCollectionName,
|
||||
PerformedBy = new StandardUser(performingUserId, true)
|
||||
};
|
||||
|
||||
SetupRepositoryMocks(sutProvider, organizationUser, organization, user);
|
||||
SetupValidatorMock(sutProvider, request, organizationUser, organization, true);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.ConfirmOrganizationUserAsync(Arg.Is<AcceptedOrganizationUserToConfirm>(o =>
|
||||
o.OrganizationUserId == organizationUser.Id && o.Key == request.Key))
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IDeviceRepository>()
|
||||
.GetManyByUserIdAsync(user.Id)
|
||||
.Returns(new List<Device> { deviceWithToken, deviceWithoutToken });
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.AutomaticallyConfirmOrganizationUserAsync(request);
|
||||
|
||||
// Assert
|
||||
Assert.True(result.IsSuccess);
|
||||
|
||||
await sutProvider.GetDependency<IPushRegistrationService>()
|
||||
.Received(1)
|
||||
.DeleteUserRegistrationOrganizationAsync(
|
||||
Arg.Is<IEnumerable<string>>(devices =>
|
||||
devices.Count(d => deviceWithToken.Id.ToString() == d) == 1),
|
||||
organization.Id.ToString());
|
||||
}
|
||||
|
||||
private static void SetupRepositoryMocks(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
OrganizationUser organizationUser,
|
||||
Organization organization,
|
||||
User user)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetByIdAsync(organizationUser.Id)
|
||||
.Returns(organizationUser);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organization.Id)
|
||||
.Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(user.Id)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IDeviceRepository>()
|
||||
.GetManyByUserIdAsync(user.Id)
|
||||
.Returns(new List<Device>());
|
||||
}
|
||||
|
||||
private static void SetupValidatorMock(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
AutomaticallyConfirmOrganizationUserRequest originalRequest,
|
||||
OrganizationUser organizationUser,
|
||||
Organization organization,
|
||||
bool isValid,
|
||||
Error? error = null)
|
||||
{
|
||||
var validationRequest = new AutomaticallyConfirmOrganizationUserValidationRequest
|
||||
{
|
||||
PerformedBy = originalRequest.PerformedBy,
|
||||
DefaultUserCollectionName = originalRequest.DefaultUserCollectionName,
|
||||
OrganizationUserId = originalRequest.OrganizationUserId,
|
||||
OrganizationUser = organizationUser,
|
||||
OrganizationId = originalRequest.OrganizationId,
|
||||
Organization = organization,
|
||||
Key = originalRequest.Key
|
||||
};
|
||||
|
||||
var validationResult = isValid
|
||||
? ValidationResultHelpers.Valid(validationRequest)
|
||||
: ValidationResultHelpers.Invalid(validationRequest, error ?? new UserIsNotAccepted());
|
||||
|
||||
sutProvider.GetDependency<IAutomaticallyConfirmOrganizationUsersValidator>()
|
||||
.ValidateAsync(Arg.Any<AutomaticallyConfirmOrganizationUserValidationRequest>())
|
||||
.Returns(validationResult);
|
||||
}
|
||||
|
||||
private static void SetupPolicyRequirementMock(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
Guid userId,
|
||||
Guid organizationId,
|
||||
bool requiresDefaultCollection)
|
||||
{
|
||||
var policyDetails = requiresDefaultCollection
|
||||
? new List<PolicyDetails> { new() { OrganizationId = organizationId } }
|
||||
: new List<PolicyDetails>();
|
||||
|
||||
var policyRequirement = new OrganizationDataOwnershipPolicyRequirement(
|
||||
requiresDefaultCollection ? OrganizationDataOwnershipState.Enabled : OrganizationDataOwnershipState.Disabled,
|
||||
policyDetails);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<OrganizationDataOwnershipPolicyRequirement>(userId)
|
||||
.Returns(policyRequirement);
|
||||
}
|
||||
|
||||
private static async Task AssertSuccessfulOperationsAsync(
|
||||
SutProvider<AutomaticallyConfirmOrganizationUserCommand> sutProvider,
|
||||
OrganizationUser organizationUser,
|
||||
Organization organization,
|
||||
User user,
|
||||
string key)
|
||||
{
|
||||
await sutProvider.GetDependency<IEventService>()
|
||||
.Received(1)
|
||||
.LogOrganizationUserEventAsync(
|
||||
Arg.Is<OrganizationUser>(x => x.Id == organizationUser.Id),
|
||||
EventType.OrganizationUser_AutomaticallyConfirmed,
|
||||
Arg.Any<DateTime?>());
|
||||
|
||||
await sutProvider.GetDependency<IMailService>()
|
||||
.Received(1)
|
||||
.SendOrganizationConfirmedEmailAsync(
|
||||
organization.Name,
|
||||
user.Email,
|
||||
organizationUser.AccessSecretsManager);
|
||||
|
||||
await sutProvider.GetDependency<IPushNotificationService>()
|
||||
.Received(1)
|
||||
.PushSyncOrgKeysAsync(user.Id);
|
||||
|
||||
await sutProvider.GetDependency<IPushRegistrationService>()
|
||||
.Received(1)
|
||||
.DeleteUserRegistrationOrganizationAsync(
|
||||
Arg.Any<IEnumerable<string>>(),
|
||||
organization.Id.ToString());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Utilities.v2;
|
||||
using Bit.Core.AdminConsole.Utilities.v2.Validation;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Entities;
|
||||
@@ -1487,8 +1488,15 @@ public class OrganizationUserRepositoryTests
|
||||
const string key = "test-key";
|
||||
orgUser.Key = key;
|
||||
|
||||
var acceptedOrganizationUser = new AcceptedOrganizationUserToConfirm
|
||||
{
|
||||
OrganizationUserId = orgUser.Id,
|
||||
UserId = user.Id,
|
||||
Key = key
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser);
|
||||
var result = await organizationUserRepository.ConfirmOrganizationUserAsync(acceptedOrganizationUser);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
@@ -1502,27 +1510,6 @@ public class OrganizationUserRepositoryTests
|
||||
await userRepository.DeleteAsync(user);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task ConfirmOrganizationUserAsync_WhenUserIsInvited_ReturnsFalse(IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository)
|
||||
{
|
||||
// Arrange
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
var orgUser = await organizationUserRepository.CreateTestOrganizationUserInviteAsync(organization);
|
||||
|
||||
// Act
|
||||
var result = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
var unchangedUser = await organizationUserRepository.GetByIdAsync(orgUser.Id);
|
||||
Assert.NotNull(unchangedUser);
|
||||
Assert.Equal(OrganizationUserStatusType.Invited, unchangedUser.Status);
|
||||
|
||||
// Annul
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task ConfirmOrganizationUserAsync_WhenUserIsAlreadyConfirmed_ReturnsFalse(IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
@@ -1533,8 +1520,17 @@ public class OrganizationUserRepositoryTests
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var orgUser = await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(organization, user);
|
||||
|
||||
orgUser.Status = OrganizationUserStatusType.Accepted; // To simulate a second call to ConfirmOrganizationUserAsync
|
||||
|
||||
var acceptedOrganizationUser = new AcceptedOrganizationUserToConfirm
|
||||
{
|
||||
OrganizationUserId = orgUser.Id,
|
||||
UserId = user.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser);
|
||||
var result = await organizationUserRepository.ConfirmOrganizationUserAsync(acceptedOrganizationUser);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
@@ -1547,30 +1543,6 @@ public class OrganizationUserRepositoryTests
|
||||
await userRepository.DeleteAsync(user);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task ConfirmOrganizationUserAsync_WhenUserIsRevoked_ReturnsFalse(IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserRepository userRepository)
|
||||
{
|
||||
// Arrange
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var orgUser = await organizationUserRepository.CreateRevokedTestOrganizationUserAsync(organization, user);
|
||||
|
||||
// Act
|
||||
var result = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
var unchangedUser = await organizationUserRepository.GetByIdAsync(orgUser.Id);
|
||||
Assert.NotNull(unchangedUser);
|
||||
Assert.Equal(OrganizationUserStatusType.Revoked, unchangedUser.Status);
|
||||
|
||||
// Annul
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
await userRepository.DeleteAsync(user);
|
||||
}
|
||||
|
||||
[Theory, DatabaseData]
|
||||
public async Task ConfirmOrganizationUserAsync_IsIdempotent_WhenCalledMultipleTimes(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@@ -1582,9 +1554,16 @@ public class OrganizationUserRepositoryTests
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var orgUser = await organizationUserRepository.CreateAcceptedTestOrganizationUserAsync(organization, user);
|
||||
|
||||
var acceptedOrganizationUser = new AcceptedOrganizationUserToConfirm
|
||||
{
|
||||
OrganizationUserId = orgUser.Id,
|
||||
UserId = user.Id,
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
// Act - First call should confirm
|
||||
var firstResult = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser);
|
||||
var secondResult = await organizationUserRepository.ConfirmOrganizationUserAsync(orgUser);
|
||||
var firstResult = await organizationUserRepository.ConfirmOrganizationUserAsync(acceptedOrganizationUser);
|
||||
var secondResult = await organizationUserRepository.ConfirmOrganizationUserAsync(acceptedOrganizationUser);
|
||||
|
||||
// Assert
|
||||
Assert.True(firstResult);
|
||||
@@ -1603,14 +1582,11 @@ public class OrganizationUserRepositoryTests
|
||||
IOrganizationUserRepository organizationUserRepository)
|
||||
{
|
||||
// Arrange
|
||||
var nonExistentUser = new OrganizationUser
|
||||
var nonExistentUser = new AcceptedOrganizationUserToConfirm
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = Guid.NewGuid(),
|
||||
OrganizationUserId = Guid.NewGuid(),
|
||||
UserId = Guid.NewGuid(),
|
||||
Email = "nonexistent@bitwarden.com",
|
||||
Status = OrganizationUserStatusType.Accepted,
|
||||
Type = OrganizationUserType.Owner
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
-- Add MaxStorageGbIncreased column to User table
|
||||
IF COL_LENGTH('[dbo].[User]', 'MaxStorageGbIncreased') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE [dbo].[User] ADD [MaxStorageGbIncreased] SMALLINT NULL;
|
||||
END
|
||||
GO
|
||||
|
||||
-- Add MaxStorageGbIncreased column to Organization table
|
||||
IF COL_LENGTH('[dbo].[Organization]', 'MaxStorageGbIncreased') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE [dbo].[Organization] ADD [MaxStorageGbIncreased] SMALLINT NULL;
|
||||
END
|
||||
GO
|
||||
|
||||
-- Update UserView to use COALESCE for MaxStorageGb
|
||||
CREATE OR ALTER VIEW [dbo].[UserView]
|
||||
AS
|
||||
SELECT
|
||||
[Id],
|
||||
[Name],
|
||||
[Email],
|
||||
[EmailVerified],
|
||||
[MasterPassword],
|
||||
[MasterPasswordHint],
|
||||
[Culture],
|
||||
[SecurityStamp],
|
||||
[TwoFactorProviders],
|
||||
[TwoFactorRecoveryCode],
|
||||
[EquivalentDomains],
|
||||
[ExcludedGlobalEquivalentDomains],
|
||||
[AccountRevisionDate],
|
||||
[Key],
|
||||
[PublicKey],
|
||||
[PrivateKey],
|
||||
[Premium],
|
||||
[PremiumExpirationDate],
|
||||
[RenewalReminderDate],
|
||||
[Storage],
|
||||
COALESCE([MaxStorageGbIncreased], [MaxStorageGb]) AS [MaxStorageGb],
|
||||
[Gateway],
|
||||
[GatewayCustomerId],
|
||||
[GatewaySubscriptionId],
|
||||
[ReferenceData],
|
||||
[LicenseKey],
|
||||
[ApiKey],
|
||||
[Kdf],
|
||||
[KdfIterations],
|
||||
[KdfMemory],
|
||||
[KdfParallelism],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[ForcePasswordReset],
|
||||
[UsesKeyConnector],
|
||||
[FailedLoginCount],
|
||||
[LastFailedLoginDate],
|
||||
[AvatarColor],
|
||||
[LastPasswordChangeDate],
|
||||
[LastKdfChangeDate],
|
||||
[LastKeyRotationDate],
|
||||
[LastEmailChangeDate],
|
||||
[VerifyDevices],
|
||||
[SecurityState],
|
||||
[SecurityVersion],
|
||||
[SignedPublicKey]
|
||||
FROM
|
||||
[dbo].[User]
|
||||
GO
|
||||
|
||||
-- Update OrganizationView to use COALESCE for MaxStorageGb
|
||||
CREATE OR ALTER VIEW [dbo].[OrganizationView]
|
||||
AS
|
||||
SELECT
|
||||
[Id],
|
||||
[Identifier],
|
||||
[Name],
|
||||
[BusinessName],
|
||||
[BusinessAddress1],
|
||||
[BusinessAddress2],
|
||||
[BusinessAddress3],
|
||||
[BusinessCountry],
|
||||
[BusinessTaxNumber],
|
||||
[BillingEmail],
|
||||
[Plan],
|
||||
[PlanType],
|
||||
[Seats],
|
||||
[MaxCollections],
|
||||
[UsePolicies],
|
||||
[UseSso],
|
||||
[UseGroups],
|
||||
[UseDirectory],
|
||||
[UseEvents],
|
||||
[UseTotp],
|
||||
[Use2fa],
|
||||
[UseApi],
|
||||
[UseResetPassword],
|
||||
[SelfHost],
|
||||
[UsersGetPremium],
|
||||
[Storage],
|
||||
COALESCE([MaxStorageGbIncreased], [MaxStorageGb]) AS [MaxStorageGb],
|
||||
[Gateway],
|
||||
[GatewayCustomerId],
|
||||
[GatewaySubscriptionId],
|
||||
[ReferenceData],
|
||||
[Enabled],
|
||||
[LicenseKey],
|
||||
[PublicKey],
|
||||
[PrivateKey],
|
||||
[TwoFactorProviders],
|
||||
[ExpirationDate],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[OwnersNotifiedOfAutoscaling],
|
||||
[MaxAutoscaleSeats],
|
||||
[UseKeyConnector],
|
||||
[UseScim],
|
||||
[UseCustomPermissions],
|
||||
[UseSecretsManager],
|
||||
[Status],
|
||||
[UsePasswordManager],
|
||||
[SmSeats],
|
||||
[SmServiceAccounts],
|
||||
[MaxAutoscaleSmSeats],
|
||||
[MaxAutoscaleSmServiceAccounts],
|
||||
[SecretsManagerBeta],
|
||||
[LimitCollectionCreation],
|
||||
[LimitCollectionDeletion],
|
||||
[LimitItemDeletion],
|
||||
[AllowAdminAccessToAllCollectionItems],
|
||||
[UseRiskInsights],
|
||||
[UseOrganizationDomains],
|
||||
[UseAdminSponsoredFamilies],
|
||||
[SyncSeats],
|
||||
[UseAutomaticUserConfirmation]
|
||||
FROM
|
||||
[dbo].[Organization]
|
||||
GO
|
||||
|
||||
|
||||
-- Update OrganizationUserOrganizationDetailsView
|
||||
CREATE OR ALTER VIEW [dbo].[OrganizationUserOrganizationDetailsView]
|
||||
AS
|
||||
SELECT
|
||||
OU.[UserId],
|
||||
OU.[OrganizationId],
|
||||
OU.[Id] OrganizationUserId,
|
||||
O.[Name],
|
||||
O.[Enabled],
|
||||
O.[PlanType],
|
||||
O.[UsePolicies],
|
||||
O.[UseSso],
|
||||
O.[UseKeyConnector],
|
||||
O.[UseScim],
|
||||
O.[UseGroups],
|
||||
O.[UseDirectory],
|
||||
O.[UseEvents],
|
||||
O.[UseTotp],
|
||||
O.[Use2fa],
|
||||
O.[UseApi],
|
||||
O.[UseResetPassword],
|
||||
O.[SelfHost],
|
||||
O.[UsersGetPremium],
|
||||
O.[UseCustomPermissions],
|
||||
O.[UseSecretsManager],
|
||||
O.[Seats],
|
||||
O.[MaxCollections],
|
||||
COALESCE(O.[MaxStorageGbIncreased], O.[MaxStorageGb]) AS [MaxStorageGb],
|
||||
O.[Identifier],
|
||||
OU.[Key],
|
||||
OU.[ResetPasswordKey],
|
||||
O.[PublicKey],
|
||||
O.[PrivateKey],
|
||||
OU.[Status],
|
||||
OU.[Type],
|
||||
SU.[ExternalId] SsoExternalId,
|
||||
OU.[Permissions],
|
||||
PO.[ProviderId],
|
||||
P.[Name] ProviderName,
|
||||
P.[Type] ProviderType,
|
||||
SS.[Enabled] SsoEnabled,
|
||||
SS.[Data] SsoConfig,
|
||||
OS.[FriendlyName] FamilySponsorshipFriendlyName,
|
||||
OS.[LastSyncDate] FamilySponsorshipLastSyncDate,
|
||||
OS.[ToDelete] FamilySponsorshipToDelete,
|
||||
OS.[ValidUntil] FamilySponsorshipValidUntil,
|
||||
OU.[AccessSecretsManager],
|
||||
O.[UsePasswordManager],
|
||||
O.[SmSeats],
|
||||
O.[SmServiceAccounts],
|
||||
O.[LimitCollectionCreation],
|
||||
O.[LimitCollectionDeletion],
|
||||
O.[AllowAdminAccessToAllCollectionItems],
|
||||
O.[UseRiskInsights],
|
||||
O.[LimitItemDeletion],
|
||||
O.[UseAdminSponsoredFamilies],
|
||||
O.[UseOrganizationDomains],
|
||||
OS.[IsAdminInitiated],
|
||||
O.[UseAutomaticUserConfirmation]
|
||||
FROM
|
||||
[dbo].[OrganizationUser] OU
|
||||
LEFT JOIN
|
||||
[dbo].[Organization] O ON O.[Id] = OU.[OrganizationId]
|
||||
LEFT JOIN
|
||||
[dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId]
|
||||
LEFT JOIN
|
||||
[dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id]
|
||||
LEFT JOIN
|
||||
[dbo].[Provider] P ON P.[Id] = PO.[ProviderId]
|
||||
LEFT JOIN
|
||||
[dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId]
|
||||
LEFT JOIN
|
||||
[dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserID] = OU.[Id]
|
||||
GO
|
||||
|
||||
-- Update ProviderUserProviderOrganizationDetailsView
|
||||
CREATE OR ALTER VIEW [dbo].[ProviderUserProviderOrganizationDetailsView]
|
||||
AS
|
||||
SELECT
|
||||
PU.[UserId],
|
||||
PO.[OrganizationId],
|
||||
O.[Name],
|
||||
O.[Enabled],
|
||||
O.[UsePolicies],
|
||||
O.[UseSso],
|
||||
O.[UseKeyConnector],
|
||||
O.[UseScim],
|
||||
O.[UseGroups],
|
||||
O.[UseDirectory],
|
||||
O.[UseEvents],
|
||||
O.[UseTotp],
|
||||
O.[Use2fa],
|
||||
O.[UseApi],
|
||||
O.[UseResetPassword],
|
||||
O.[UseSecretsManager],
|
||||
O.[UsePasswordManager],
|
||||
O.[SelfHost],
|
||||
O.[UsersGetPremium],
|
||||
O.[UseCustomPermissions],
|
||||
O.[Seats],
|
||||
O.[MaxCollections],
|
||||
COALESCE(O.[MaxStorageGbIncreased], O.[MaxStorageGb]) AS [MaxStorageGb],
|
||||
O.[Identifier],
|
||||
PO.[Key],
|
||||
O.[PublicKey],
|
||||
O.[PrivateKey],
|
||||
PU.[Status],
|
||||
PU.[Type],
|
||||
PO.[ProviderId],
|
||||
PU.[Id] ProviderUserId,
|
||||
P.[Name] ProviderName,
|
||||
O.[PlanType],
|
||||
O.[LimitCollectionCreation],
|
||||
O.[LimitCollectionDeletion],
|
||||
O.[AllowAdminAccessToAllCollectionItems],
|
||||
O.[UseRiskInsights],
|
||||
O.[UseAdminSponsoredFamilies],
|
||||
P.[Type] ProviderType,
|
||||
O.[LimitItemDeletion],
|
||||
O.[UseOrganizationDomains],
|
||||
O.[UseAutomaticUserConfirmation],
|
||||
SS.[Enabled] SsoEnabled,
|
||||
SS.[Data] SsoConfig
|
||||
FROM
|
||||
[dbo].[ProviderUser] PU
|
||||
INNER JOIN
|
||||
[dbo].[ProviderOrganization] PO ON PO.[ProviderId] = PU.[ProviderId]
|
||||
INNER JOIN
|
||||
[dbo].[Organization] O ON O.[Id] = PO.[OrganizationId]
|
||||
INNER JOIN
|
||||
[dbo].[Provider] P ON P.[Id] = PU.[ProviderId]
|
||||
LEFT JOIN
|
||||
[dbo].[SsoConfig] SS ON SS.[OrganizationId] = O.[Id]
|
||||
GO
|
||||
|
||||
-- Refresh views that reference Organization table
|
||||
EXEC sp_refreshview N'[dbo].[OrganizationCipherDetailsCollectionsView]';
|
||||
EXEC sp_refreshview N'[dbo].[OrganizationUserOrganizationDetailsView]';
|
||||
EXEC sp_refreshview N'[dbo].[ProviderOrganizationOrganizationDetailsView]';
|
||||
EXEC sp_refreshview N'[dbo].[ProviderUserProviderOrganizationDetailsView]';
|
||||
GO
|
||||
|
||||
-- Refresh views that reference User table
|
||||
EXEC sp_refreshview N'[dbo].[EmergencyAccessDetailsView]';
|
||||
EXEC sp_refreshview N'[dbo].[OrganizationUserUserDetailsView]';
|
||||
EXEC sp_refreshview N'[dbo].[ProviderUserUserDetailsView]';
|
||||
EXEC sp_refreshview N'[dbo].[UserEmailDomainView]';
|
||||
GO
|
||||
|
||||
-- Refresh stored procedures that reference UserView
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[Notification_ReadByUserIdAndStatus]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadById]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByIds]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByOrganizationId]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByOrganizationIdEmail]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByOrganizationIdUserId]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByOrganizationIdWithClaimedDomains]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByUserId]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByUserIds]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadByUserIdWithPolicyDetails]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUser_ReadOccupiedSmSeatCountByOrganizationId]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[ProviderUser_ReadById]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[ProviderUser_ReadByIds]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[ProviderUser_ReadByOrganizationIdStatus]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[ProviderUser_ReadByProviderId]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[ProviderUser_ReadByProviderIdUserId]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[ProviderUser_ReadByUserId]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[User_ReadByEmail]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[User_ReadByEmails]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[User_ReadById]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[User_ReadByIds]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[User_ReadByIdsWithCalculatedPremium]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[User_ReadByPremium]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[User_ReadBySsoUserOrganizationIdExternalId]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[User_Search]';
|
||||
GO
|
||||
|
||||
-- Refresh stored procedures that reference OrganizationView
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[Organization_GetOrganizationsForSubscriptionSync]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadByClaimedUserEmailDomain]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadByEnabled]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadById]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadByIdentifier]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadByLicenseKey]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadByProviderId]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadByUserId]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadManyByIds]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[Organization_ReadOccupiedSeatCountByOrganizationId]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[Organization_Search]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[Organization_UnassignedToProviderSearch]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[OrganizationDomainSsoDetails_ReadByEmail]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[PolicyDetails_ReadByOrganizationId]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[PolicyDetails_ReadByUserId]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[PolicyDetails_ReadByUserIdsPolicyType]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[ProviderOrganization_ReadById]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[ProviderOrganization_ReadByOrganizationId]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[ProviderOrganization_ReadCountByOrganizationIds]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[ProviderOrganizationProviderDetails_ReadByUserId]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[VerifiedOrganizationDomainSsoDetails_ReadByEmail]';
|
||||
GO
|
||||
|
||||
-- Refresh stored procedures that reference OrganizationUserOrganizationDetailsView
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatus]';
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatusOrganizationId]';
|
||||
GO
|
||||
|
||||
-- Refresh stored procedures that reference ProviderUserProviderOrganizationDetailsView
|
||||
EXEC sp_refreshsqlmodule N'[dbo].[ProviderUserProviderOrganizationDetails_ReadByUserIdStatus]';
|
||||
GO
|
||||
|
||||
3446
util/MySqlMigrations/Migrations/20251112155802_AddMaxStorageGbIncreasedColumn.Designer.cs
generated
Normal file
3446
util/MySqlMigrations/Migrations/20251112155802_AddMaxStorageGbIncreasedColumn.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.MySqlMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddMaxStorageGbIncreasedColumn : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<short>(
|
||||
name: "MaxStorageGbIncreased",
|
||||
table: "User",
|
||||
type: "smallint",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<short>(
|
||||
name: "MaxStorageGbIncreased",
|
||||
table: "Organization",
|
||||
type: "smallint",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxStorageGbIncreased",
|
||||
table: "User");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxStorageGbIncreased",
|
||||
table: "Organization");
|
||||
}
|
||||
}
|
||||
3452
util/PostgresMigrations/Migrations/20251112155845_AddMaxStorageGbIncreasedColumn.Designer.cs
generated
Normal file
3452
util/PostgresMigrations/Migrations/20251112155845_AddMaxStorageGbIncreasedColumn.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.PostgresMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddMaxStorageGbIncreasedColumn : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<short>(
|
||||
name: "MaxStorageGbIncreased",
|
||||
table: "User",
|
||||
type: "smallint",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<short>(
|
||||
name: "MaxStorageGbIncreased",
|
||||
table: "Organization",
|
||||
type: "smallint",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxStorageGbIncreased",
|
||||
table: "User");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxStorageGbIncreased",
|
||||
table: "Organization");
|
||||
}
|
||||
}
|
||||
3435
util/SqliteMigrations/Migrations/20251112155857_AddMaxStorageGbIncreasedColumn.Designer.cs
generated
Normal file
3435
util/SqliteMigrations/Migrations/20251112155857_AddMaxStorageGbIncreasedColumn.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.SqliteMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddMaxStorageGbIncreasedColumn : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<short>(
|
||||
name: "MaxStorageGbIncreased",
|
||||
table: "User",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<short>(
|
||||
name: "MaxStorageGbIncreased",
|
||||
table: "Organization",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxStorageGbIncreased",
|
||||
table: "User");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxStorageGbIncreased",
|
||||
table: "Organization");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user