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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Models;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Enforcement.AutoConfirm;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
Expand All @@ -23,6 +24,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
{
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IGetOrganizationUserQuery _getOrganizationUserQuery;
private readonly IUserRepository _userRepository;
private readonly IEventService _eventService;
private readonly IMailService _mailService;
Expand All @@ -39,6 +41,7 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand
public ConfirmOrganizationUserCommand(
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IGetOrganizationUserQuery getOrganizationUserQuery,
IUserRepository userRepository,
IEventService eventService,
IMailService mailService,
Expand All @@ -54,6 +57,7 @@ public ConfirmOrganizationUserCommand(
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_getOrganizationUserQuery = getOrganizationUserQuery;
_userRepository = userRepository;
_eventService = eventService;
_mailService = mailService;
Expand Down Expand Up @@ -117,25 +121,27 @@ public async Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid
private async Task<List<Tuple<OrganizationUser, string>>> SaveChangesToDatabaseAsync(Guid organizationId, Dictionary<Guid, string> keys,
Guid confirmingUserId)
{
var selectedOrganizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys);
// Use the query to fetch strongly-typed models instead of raw entities
var selectedOrganizationUsers = await _getOrganizationUserQuery.GetManyOrganizationUsersAsync(keys.Keys);
var validSelectedOrganizationUsers = selectedOrganizationUsers
.Where(u => u.Status == OrganizationUserStatusType.Accepted && u.OrganizationId == organizationId && u.UserId != null)
.OfType<AcceptedOrganizationUser>()
.Where(ou => !ou.Revoked)
.ToList();

if (!validSelectedOrganizationUsers.Any())
{
return new List<Tuple<OrganizationUser, string>>();
}

var validSelectedUserIds = validSelectedOrganizationUsers.Select(u => u.UserId.Value).ToList();
var validSelectedUserIds = validSelectedOrganizationUsers.Select(u => u.UserId).ToList();

var organization = await _organizationRepository.GetByIdAsync(organizationId);
var allUsersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validSelectedUserIds);

var users = await _userRepository.GetManyAsync(validSelectedUserIds);
var usersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(validSelectedUserIds);

var keyedFilteredUsers = validSelectedOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u);
var keyedFilteredUsers = validSelectedOrganizationUsers.ToDictionary(u => u.UserId, u => u);
var keyedOrganizationUsers = allUsersOrgs.GroupBy(u => u.UserId.Value)
.ToDictionary(u => u.Key, u => u.ToList());

Expand Down Expand Up @@ -165,18 +171,21 @@ private async Task<List<Tuple<OrganizationUser, string>>> SaveChangesToDatabaseA

var userTwoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;
await CheckPoliciesAsync(organizationId, user, orgUsers, userTwoFactorEnabled);
orgUser.Status = OrganizationUserStatusType.Confirmed;
orgUser.Key = keys[orgUser.Id];
orgUser.Email = null;

await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, orgUser.AccessSecretsManager);
succeededUsers.Add(orgUser);
result.Add(Tuple.Create(orgUser, ""));

// Use the strongly-typed model to transition from Accepted to Confirmed state
var confirmedOrgUser = orgUser.ToConfirmed(keys[orgUser.Id]);

// Ideally these would accept an interface, but for now we'll convert it back
var confirmedEntity = confirmedOrgUser.ToEntity();
await _eventService.LogOrganizationUserEventAsync(confirmedEntity, EventType.OrganizationUser_Confirmed);
await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, confirmedOrgUser.AccessSecretsManager);

succeededUsers.Add(confirmedEntity);
result.Add(Tuple.Create(confirmedEntity, ""));
}
catch (BadRequestException e)
{
result.Add(Tuple.Create(orgUser, e.Message));
result.Add(Tuple.Create(orgUser.ToEntity(), e.Message));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
๏ปฟusing Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Models;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;

namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;

public class GetOrganizationUserQuery(IOrganizationUserRepository organizationUserRepository)
: IGetOrganizationUserQuery
{
public async Task<ITypedOrganizationUser?> GetOrganizationUserAsync(Guid organizationUserId)
{
var organizationUser = await organizationUserRepository.GetByIdAsync(organizationUserId);

if (organizationUser == null)
{
return null;
}

return ConvertToStronglyTypedModel(organizationUser);
}

public async Task<IEnumerable<ITypedOrganizationUser>> GetManyOrganizationUsersAsync(IEnumerable<Guid> organizationUserIds)
{
var organizationUsers = await organizationUserRepository.GetManyAsync(organizationUserIds);

return organizationUsers
.Select(ConvertToStronglyTypedModel)
.ToList();
}

private static ITypedOrganizationUser ConvertToStronglyTypedModel(OrganizationUser organizationUser)
{
// Determine the appropriate model type based on the status
// For revoked users, use GetPriorActiveOrganizationUserStatusType to determine the underlying status
var effectiveStatus = organizationUser.Status == OrganizationUserStatusType.Revoked
? OrganizationService.GetPriorActiveOrganizationUserStatusType(organizationUser)
: organizationUser.Status;

return effectiveStatus switch
{
OrganizationUserStatusType.Invited => InvitedOrganizationUser.FromEntity(organizationUser),
OrganizationUserStatusType.Accepted => AcceptedOrganizationUser.FromEntity(organizationUser),
OrganizationUserStatusType.Confirmed => ConfirmedOrganizationUser.FromEntity(organizationUser),
_ => throw new InvalidOperationException($"Unsupported organization user status: {organizationUser.Status}")
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
๏ปฟnamespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;

public interface IGetOrganizationUserQuery
{
/// <summary>
/// Retrieves an organization user by their ID and returns the appropriate strongly-typed model
/// based on their status (Invited, Accepted, Confirmed, or Revoked).
/// </summary>
/// <param name="organizationUserId">The ID of the organization user to retrieve.</param>
Task<ITypedOrganizationUser?> GetOrganizationUserAsync(Guid organizationUserId);

/// <summary>
/// Retrieves multiple organization users by their IDs and returns the appropriate strongly-typed models
/// based on their status (Invited, Accepted, Confirmed, or Revoked).
/// </summary>
/// <param name="organizationUserIds">The IDs of the organization users to retrieve.</param>
Task<IEnumerable<ITypedOrganizationUser>> GetManyOrganizationUsersAsync(IEnumerable<Guid> organizationUserIds);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
๏ปฟ#nullable enable

using Bit.Core.Models.Data;
using Bit.Core.Utilities;

namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;

/// <summary>
/// Represents an entity that has organization user permissions stored as a JSON string.
/// </summary>
public interface IOrganizationUserPermissions
{
/// <summary>
/// A json blob representing the <see cref="Bit.Core.Models.Data.Permissions"/> of the OrganizationUser if they
/// are a Custom user role. MAY be NULL if they are not a custom user, but this is not guaranteed;
/// do not use this to determine their role.
/// </summary>
/// <remarks>
/// Avoid using this property directly - instead use the extension methods
/// <see cref="OrganizationUserPermissionsExtensions.GetPermissions"/> and
/// <see cref="OrganizationUserPermissionsExtensions.SetPermissions"/>.
/// </remarks>
string? Permissions { get; set; }
}

/// <summary>
/// Extension methods for working with <see cref="IOrganizationUserPermissions"/> implementations.
/// </summary>
public static class OrganizationUserPermissionsExtensions
{
/// <summary>
/// Deserializes the Permissions JSON string into a <see cref="Permissions"/> object.
/// </summary>
/// <param name="organizationUser">The organization user with permissions.</param>
/// <returns>A <see cref="Permissions"/> object if the JSON is valid, otherwise null.</returns>
public static Permissions? GetPermissions(this IOrganizationUserPermissions organizationUser)
{
return string.IsNullOrWhiteSpace(organizationUser.Permissions) ? null
: CoreHelpers.LoadClassFromJsonData<Permissions>(organizationUser.Permissions);
}

/// <summary>
/// Serializes a <see cref="Permissions"/> object into a JSON string and stores it.
/// </summary>
/// <param name="organizationUser">The organization user to set permissions on.</param>
/// <param name="permissions">The permissions object to serialize.</param>
public static void SetPermissions(this IOrganizationUserPermissions organizationUser, Permissions permissions)
{
organizationUser.Permissions = CoreHelpers.ClassToJsonData(permissions);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
๏ปฟusing Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models;

namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;

/// <summary>
/// Represents common properties shared by all typed organization user models.
/// </summary>
public interface ITypedOrganizationUser : IExternal, IOrganizationUserPermissions
{
/// <summary>
/// A unique identifier for the organization user.
/// </summary>
Guid Id { get; set; }

/// <summary>
/// The ID of the Organization.
/// </summary>
Guid OrganizationId { get; set; }

/// <summary>
/// The User's role in the Organization.
/// </summary>
OrganizationUserType Type { get; set; }

/// <summary>
/// The date the OrganizationUser was created.
/// </summary>
DateTime CreationDate { get; }

/// <summary>
/// The last date the OrganizationUser entry was updated.
/// </summary>
DateTime RevisionDate { get; }

/// <summary>
/// True if the User has access to Secrets Manager for this Organization, false otherwise.
/// </summary>
bool AccessSecretsManager { get; set; }

/// <summary>
/// True if the user's access has been revoked, false otherwise.
/// </summary>
bool Revoked { get; set; }

public OrganizationUser ToEntity();
}
Loading
Loading