mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[AC-2614] Member Access Report Endpoint (#4599)
* Initial draft of moving the org user controller details method into a query * Removing comments and addressing pr items * Adding the org users query to core * Adding the member access report * Addressing some pr concerns and refactoring to be more efficient * Some minor changes to the way properties are spelled * Setting authorization to organization * Adding the permissions check for reports and comments * removing unnecessary usings * Removing ciphers controller change that was a mistake * There was a duplication issue in getting collections for users grabbing groups * Adding comments to the CreateReport method * Only get the user collections by userId * Some finaly refactoring * Adding the no group, no collection, and no perms local strings * Modifying and adding query test cases * Removing unnecessary permissions code in query * Added mapping for id and UsesKeyConnector to MemberAccessReportModel (#4681) * Moving test cases from controller fully into the query. --------- Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Co-authored-by: aj-rosado <109146700+aj-rosado@users.noreply.github.com>
This commit is contained in:
parent
fdf6d8f9c3
commit
af3797c540
@ -23,6 +23,8 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -49,8 +51,10 @@ public class OrganizationUsersController : Controller
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
|
||||
|
||||
public OrganizationUsersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -69,6 +73,7 @@ public class OrganizationUsersController : Controller
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -88,6 +93,7 @@ public class OrganizationUsersController : Controller
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_featureService = featureService;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_organizationUserUserDetailsQuery = organizationUserUserDetailsQuery;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
}
|
||||
|
||||
@ -135,23 +141,21 @@ public class OrganizationUsersController : Controller
|
||||
return await Get_vNext(orgId, includeGroups, includeCollections);
|
||||
}
|
||||
|
||||
var organizationUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(orgId, includeGroups, includeCollections);
|
||||
var organizationUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails(
|
||||
new OrganizationUserUserDetailsQueryRequest
|
||||
{
|
||||
OrganizationId = orgId,
|
||||
IncludeGroups = includeGroups,
|
||||
IncludeCollections = includeCollections
|
||||
}
|
||||
);
|
||||
|
||||
var responseTasks = organizationUsers
|
||||
.Select(async o =>
|
||||
{
|
||||
var orgUser = new OrganizationUserUserDetailsResponseModel(o,
|
||||
await _userService.TwoFactorIsEnabledAsync(o));
|
||||
|
||||
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
|
||||
orgUser.Type = GetFlexibleCollectionsUserType(orgUser.Type, orgUser.Permissions);
|
||||
|
||||
// Set 'Edit/Delete Assigned Collections' custom permissions to false
|
||||
if (orgUser.Permissions is not null)
|
||||
{
|
||||
orgUser.Permissions.EditAssignedCollections = false;
|
||||
orgUser.Permissions.DeleteAssignedCollections = false;
|
||||
}
|
||||
|
||||
return orgUser;
|
||||
});
|
||||
var responses = await Task.WhenAll(responseTasks);
|
||||
@ -666,28 +670,23 @@ public class OrganizationUsersController : Controller
|
||||
private async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get_vNext(Guid orgId,
|
||||
bool includeGroups = false, bool includeCollections = false)
|
||||
{
|
||||
var organizationUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(orgId, includeGroups, includeCollections);
|
||||
var organizationUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails(
|
||||
new OrganizationUserUserDetailsQueryRequest
|
||||
{
|
||||
OrganizationId = orgId,
|
||||
IncludeGroups = includeGroups,
|
||||
IncludeCollections = includeCollections
|
||||
}
|
||||
);
|
||||
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers);
|
||||
var responseTasks = organizationUsers
|
||||
.Select(async o =>
|
||||
var responses = organizationUsers
|
||||
.Select(o =>
|
||||
{
|
||||
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == o.Id).twoFactorIsEnabled;
|
||||
var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled);
|
||||
|
||||
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
|
||||
orgUser.Type = GetFlexibleCollectionsUserType(orgUser.Type, orgUser.Permissions);
|
||||
|
||||
// Set 'Edit/Delete Assigned Collections' custom permissions to false
|
||||
if (orgUser.Permissions is not null)
|
||||
{
|
||||
orgUser.Permissions.EditAssignedCollections = false;
|
||||
orgUser.Permissions.DeleteAssignedCollections = false;
|
||||
}
|
||||
|
||||
return orgUser;
|
||||
});
|
||||
var responses = await Task.WhenAll(responseTasks);
|
||||
|
||||
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(responses);
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,8 @@ public class OrganizationUserResponseModel : ResponseModel
|
||||
ResetPasswordEnrolled = !string.IsNullOrEmpty(organizationUser.ResetPasswordKey);
|
||||
}
|
||||
|
||||
public OrganizationUserResponseModel(OrganizationUserUserDetails organizationUser, string obj = "organizationUser")
|
||||
public OrganizationUserResponseModel(OrganizationUserUserDetails organizationUser,
|
||||
string obj = "organizationUser")
|
||||
: base(obj)
|
||||
{
|
||||
if (organizationUser == null)
|
||||
@ -105,7 +106,6 @@ public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponse
|
||||
ResetPasswordEnrolled = ResetPasswordEnrolled && !organizationUser.UsesKeyConnector;
|
||||
}
|
||||
|
||||
|
||||
public string Name { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string AvatarColor { get; set; }
|
||||
|
@ -33,6 +33,7 @@ using Bit.Core.Vault.Entities;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
|
||||
|
||||
#if !OSS
|
||||
using Bit.Commercial.Core.SecretsManager;
|
||||
using Bit.Commercial.Core.Utilities;
|
||||
|
77
src/Api/Tools/Controllers/ReportsController.cs
Normal file
77
src/Api/Tools/Controllers/ReportsController.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using Bit.Api.Tools.Models.Response;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Vault.Queries;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Tools.Controllers;
|
||||
|
||||
[Route("reports")]
|
||||
[Authorize("Application")]
|
||||
public class ReportsController : Controller
|
||||
{
|
||||
private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery;
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
|
||||
public ReportsController(
|
||||
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
|
||||
IGroupRepository groupRepository,
|
||||
ICollectionRepository collectionRepository,
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationCiphersQuery organizationCiphersQuery,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery
|
||||
)
|
||||
{
|
||||
_organizationUserUserDetailsQuery = organizationUserUserDetailsQuery;
|
||||
_groupRepository = groupRepository;
|
||||
_collectionRepository = collectionRepository;
|
||||
_currentContext = currentContext;
|
||||
_organizationCiphersQuery = organizationCiphersQuery;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
}
|
||||
|
||||
[HttpGet("member-access/{orgId}")]
|
||||
public async Task<IEnumerable<MemberAccessReportResponseModel>> GetMemberAccessReport(Guid orgId)
|
||||
{
|
||||
if (!await _currentContext.AccessReports(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var orgUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails(
|
||||
new OrganizationUserUserDetailsQueryRequest
|
||||
{
|
||||
OrganizationId = orgId,
|
||||
IncludeCollections = true,
|
||||
IncludeGroups = true
|
||||
});
|
||||
|
||||
var orgGroups = await _groupRepository.GetManyByOrganizationIdAsync(orgId);
|
||||
var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId);
|
||||
var orgCollectionsWithAccess = await _collectionRepository.GetManyByOrganizationIdWithAccessAsync(orgId);
|
||||
var orgItems = await _organizationCiphersQuery.GetAllOrganizationCiphers(orgId);
|
||||
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
|
||||
|
||||
var reports = MemberAccessReportResponseModel.CreateReport(
|
||||
orgGroups,
|
||||
orgCollectionsWithAccess,
|
||||
orgItems,
|
||||
organizationUsersTwoFactorEnabled,
|
||||
orgAbility);
|
||||
return reports;
|
||||
}
|
||||
}
|
172
src/Api/Tools/Models/Response/MemberAccessReportModel.cs
Normal file
172
src/Api/Tools/Models/Response/MemberAccessReportModel.cs
Normal file
@ -0,0 +1,172 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
|
||||
namespace Bit.Api.Tools.Models.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Member access details. The individual item for the detailed member access
|
||||
/// report. A collection can be assigned directly to a user without a group or
|
||||
/// the user can be assigned to a collection through a group. Group level permissions
|
||||
/// can override collection level permissions.
|
||||
/// </summary>
|
||||
public class MemberAccessReportAccessDetails
|
||||
{
|
||||
public Guid? CollectionId { get; set; }
|
||||
public Guid? GroupId { get; set; }
|
||||
public string GroupName { get; set; }
|
||||
public string CollectionName { get; set; }
|
||||
public int ItemCount { get; set; }
|
||||
public bool? ReadOnly { get; set; }
|
||||
public bool? HidePasswords { get; set; }
|
||||
public bool? Manage { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains the collections and group collections a user has access to including
|
||||
/// the permission level for the collection and group collection.
|
||||
/// </summary>
|
||||
public class MemberAccessReportResponseModel
|
||||
{
|
||||
public string UserName { get; set; }
|
||||
public string Email { get; set; }
|
||||
public bool TwoFactorEnabled { get; set; }
|
||||
public bool AccountRecoveryEnabled { get; set; }
|
||||
public int GroupsCount { get; set; }
|
||||
public int CollectionsCount { get; set; }
|
||||
public int TotalItemCount { get; set; }
|
||||
public Guid? UserGuid { get; set; }
|
||||
public bool UsesKeyConnector { get; set; }
|
||||
public IEnumerable<MemberAccessReportAccessDetails> AccessDetails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Generates a report for all members of an organization. Containing summary information
|
||||
/// such as item, collection, and group counts. As well as detailed information on the
|
||||
/// user and group collections along with their permissions
|
||||
/// </summary>
|
||||
/// <param name="orgGroups">Organization groups collection</param>
|
||||
/// <param name="orgCollectionsWithAccess">Collections for the organization and the groups/users and permissions</param>
|
||||
/// <param name="orgItems">Cipher items for the organization with the collections associated with them</param>
|
||||
/// <param name="organizationUsersTwoFactorEnabled">Organization users and two factor status</param>
|
||||
/// <param name="orgAbility">Organization ability for account recovery status</param>
|
||||
/// <returns>List of the MemberAccessReportResponseModel</returns>;
|
||||
public static IEnumerable<MemberAccessReportResponseModel> CreateReport(
|
||||
ICollection<Group> orgGroups,
|
||||
ICollection<Tuple<Collection, CollectionAccessDetails>> orgCollectionsWithAccess,
|
||||
IEnumerable<CipherOrganizationDetailsWithCollections> orgItems,
|
||||
IEnumerable<(OrganizationUserUserDetails user, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled,
|
||||
OrganizationAbility orgAbility)
|
||||
{
|
||||
var orgUsers = organizationUsersTwoFactorEnabled.Select(x => x.user);
|
||||
// Create a dictionary to lookup the group names later.
|
||||
var groupNameDictionary = orgGroups.ToDictionary(x => x.Id, x => x.Name);
|
||||
|
||||
// Get collections grouped and into a dictionary for counts
|
||||
var collectionItems = orgItems
|
||||
.SelectMany(x => x.CollectionIds,
|
||||
(x, b) => new { CipherId = x.Id, CollectionId = b })
|
||||
.GroupBy(y => y.CollectionId,
|
||||
(key, g) => new { CollectionId = key, Ciphers = g });
|
||||
var collectionItemCounts = collectionItems.ToDictionary(x => x.CollectionId, x => x.Ciphers.Count());
|
||||
|
||||
// Take the collections/groups and create the access details items
|
||||
var groupAccessDetails = new List<MemberAccessReportAccessDetails>();
|
||||
var userCollectionAccessDetails = new List<MemberAccessReportAccessDetails>();
|
||||
foreach (var tCollect in orgCollectionsWithAccess)
|
||||
{
|
||||
var itemCounts = collectionItemCounts.TryGetValue(tCollect.Item1.Id, out var itemCount) ? itemCount : 0;
|
||||
if (tCollect.Item2.Groups.Count() > 0)
|
||||
{
|
||||
var groupDetails = tCollect.Item2.Groups.Select(x =>
|
||||
new MemberAccessReportAccessDetails
|
||||
{
|
||||
CollectionId = tCollect.Item1.Id,
|
||||
CollectionName = tCollect.Item1.Name,
|
||||
GroupId = x.Id,
|
||||
GroupName = groupNameDictionary[x.Id],
|
||||
ReadOnly = x.ReadOnly,
|
||||
HidePasswords = x.HidePasswords,
|
||||
Manage = x.Manage,
|
||||
ItemCount = itemCounts,
|
||||
});
|
||||
groupAccessDetails.AddRange(groupDetails);
|
||||
}
|
||||
|
||||
// All collections assigned to users and their permissions
|
||||
if (tCollect.Item2.Users.Count() > 0)
|
||||
{
|
||||
var userCollectionDetails = tCollect.Item2.Users.Select(x =>
|
||||
new MemberAccessReportAccessDetails
|
||||
{
|
||||
CollectionId = tCollect.Item1.Id,
|
||||
CollectionName = tCollect.Item1.Name,
|
||||
ReadOnly = x.ReadOnly,
|
||||
HidePasswords = x.HidePasswords,
|
||||
Manage = x.Manage,
|
||||
ItemCount = itemCounts,
|
||||
});
|
||||
userCollectionAccessDetails.AddRange(userCollectionDetails);
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through the org users and populate report and access data
|
||||
var memberAccessReport = new List<MemberAccessReportResponseModel>();
|
||||
foreach (var user in orgUsers)
|
||||
{
|
||||
var report = new MemberAccessReportResponseModel
|
||||
{
|
||||
UserName = user.Name,
|
||||
Email = user.Email,
|
||||
TwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == user.Id).twoFactorIsEnabled,
|
||||
// Both the user's ResetPasswordKey must be set and the organization can UseResetPassword
|
||||
AccountRecoveryEnabled = !string.IsNullOrEmpty(user.ResetPasswordKey) && orgAbility.UseResetPassword,
|
||||
UserGuid = user.Id,
|
||||
UsesKeyConnector = user.UsesKeyConnector
|
||||
};
|
||||
|
||||
var userAccessDetails = new List<MemberAccessReportAccessDetails>();
|
||||
if (user.Groups.Any())
|
||||
{
|
||||
var userGroups = groupAccessDetails.Where(x => user.Groups.Contains(x.GroupId.GetValueOrDefault()));
|
||||
userAccessDetails.AddRange(userGroups);
|
||||
}
|
||||
|
||||
// There can be edge cases where groups don't have a collection
|
||||
var groupsWithoutCollections = user.Groups.Where(x => !userAccessDetails.Any(y => x == y.GroupId));
|
||||
if (groupsWithoutCollections.Count() > 0)
|
||||
{
|
||||
var emptyGroups = groupsWithoutCollections.Select(x => new MemberAccessReportAccessDetails
|
||||
{
|
||||
GroupId = x,
|
||||
GroupName = groupNameDictionary[x],
|
||||
ItemCount = 0
|
||||
});
|
||||
userAccessDetails.AddRange(emptyGroups);
|
||||
}
|
||||
|
||||
if (user.Collections.Any())
|
||||
{
|
||||
var userCollections = userCollectionAccessDetails.Where(x => user.Collections.Any(y => x.CollectionId == y.Id));
|
||||
userAccessDetails.AddRange(userCollections);
|
||||
}
|
||||
report.AccessDetails = userAccessDetails;
|
||||
|
||||
report.TotalItemCount = collectionItems
|
||||
.Where(x => report.AccessDetails.Any(y => x.CollectionId == y.CollectionId))
|
||||
.SelectMany(x => x.Ciphers)
|
||||
.GroupBy(g => g.CipherId).Select(grp => grp.FirstOrDefault())
|
||||
.Count();
|
||||
|
||||
// Distinct items only
|
||||
var distinctItems = report.AccessDetails.Where(x => x.CollectionId.HasValue).Select(x => x.CollectionId).Distinct();
|
||||
report.CollectionsCount = distinctItems.Count();
|
||||
report.GroupsCount = report.AccessDetails.Select(x => x.GroupId).Where(y => y.HasValue).Distinct().Count();
|
||||
memberAccessReport.Add(report);
|
||||
}
|
||||
return memberAccessReport;
|
||||
}
|
||||
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
namespace Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
|
||||
namespace Bit.Core.Enums;
|
||||
|
||||
public enum OrganizationUserType : byte
|
||||
{
|
||||
@ -8,3 +10,35 @@ public enum OrganizationUserType : byte
|
||||
// Manager = 3 has been intentionally permanently deleted
|
||||
Custom = 4,
|
||||
}
|
||||
|
||||
public static class OrganizationUserTypeExtensions
|
||||
{
|
||||
public static OrganizationUserType GetFlexibleCollectionsUserType(this OrganizationUserType type, Permissions permissions)
|
||||
{
|
||||
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
|
||||
if (type == OrganizationUserType.Custom && permissions is not null)
|
||||
{
|
||||
if ((permissions.EditAssignedCollections || permissions.DeleteAssignedCollections) &&
|
||||
permissions is
|
||||
{
|
||||
AccessEventLogs: false,
|
||||
AccessImportExport: false,
|
||||
AccessReports: false,
|
||||
CreateNewCollections: false,
|
||||
EditAnyCollection: false,
|
||||
DeleteAnyCollection: false,
|
||||
ManageGroups: false,
|
||||
ManagePolicies: false,
|
||||
ManageSso: false,
|
||||
ManageUsers: false,
|
||||
ManageResetPassword: false,
|
||||
ManageScim: false
|
||||
})
|
||||
{
|
||||
return OrganizationUserType.User;
|
||||
}
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
|
||||
namespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
|
||||
public interface IOrganizationUserUserDetailsQuery
|
||||
{
|
||||
Task<IEnumerable<OrganizationUserUserDetails>> GetOrganizationUserUserDetails(OrganizationUserUserDetailsQueryRequest request);
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
|
||||
namespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
|
||||
public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuery
|
||||
{
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
|
||||
public OrganizationUserUserDetailsQuery(
|
||||
IOrganizationUserRepository organizationUserRepository
|
||||
)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the organization user user details for the provided request
|
||||
/// </summary>
|
||||
/// <param name="request">Request details for the query</param>
|
||||
/// <returns>List of OrganizationUserUserDetails</returns>
|
||||
public async Task<IEnumerable<OrganizationUserUserDetails>> GetOrganizationUserUserDetails(OrganizationUserUserDetailsQueryRequest request)
|
||||
{
|
||||
var organizationUsers = await _organizationUserRepository
|
||||
.GetManyDetailsByOrganizationAsync(request.OrganizationId, request.IncludeGroups, request.IncludeCollections);
|
||||
|
||||
return organizationUsers
|
||||
.Select(o =>
|
||||
{
|
||||
var userPermissions = o.GetPermissions();
|
||||
|
||||
// Downgrade Custom users with no other permissions than 'Edit/Delete Assigned Collections' to User
|
||||
o.Type = o.Type.GetFlexibleCollectionsUserType(userPermissions);
|
||||
|
||||
if (userPermissions is not null)
|
||||
{
|
||||
userPermissions.EditAssignedCollections = false;
|
||||
userPermissions.DeleteAssignedCollections = false;
|
||||
}
|
||||
|
||||
o.Permissions = CoreHelpers.ClassToJsonData(userPermissions);
|
||||
|
||||
return o;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
namespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
|
||||
public class OrganizationUserUserDetailsQueryRequest
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public bool IncludeGroups { get; set; } = false;
|
||||
public bool IncludeCollections { get; set; } = false;
|
||||
}
|
@ -26,6 +26,8 @@ using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -136,6 +138,7 @@ public static class OrganizationServiceCollectionExtensions
|
||||
{
|
||||
services.AddScoped<ICountNewSmSeatsRequiredQuery, CountNewSmSeatsRequiredQuery>();
|
||||
services.AddScoped<IAcceptOrgUserCommand, AcceptOrgUserCommand>();
|
||||
services.AddScoped<IOrganizationUserUserDetailsQuery, OrganizationUserUserDetailsQuery>();
|
||||
}
|
||||
|
||||
// TODO: move to OrganizationSubscriptionServiceCollectionExtensions when OrganizationUser methods are moved out of
|
||||
|
@ -13,7 +13,6 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
@ -22,6 +21,8 @@ using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
@ -194,71 +195,6 @@ public class OrganizationUsersControllerTests
|
||||
Assert.True(response.Data.All(r => organizationUsers.Any(ou => ou.Id == r.Id)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Get_HandlesNullPermissionsObject(
|
||||
ICollection<OrganizationUserUserDetails> organizationUsers, OrganizationAbility organizationAbility,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
Get_Setup(organizationAbility, organizationUsers, sutProvider);
|
||||
organizationUsers.First().Permissions = "null";
|
||||
var response = await sutProvider.Sut.Get(organizationAbility.Id);
|
||||
|
||||
Assert.True(response.Data.All(r => organizationUsers.Any(ou => ou.Id == r.Id)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Get_SetsDeprecatedCustomPermissionstoFalse(
|
||||
ICollection<OrganizationUserUserDetails> organizationUsers, OrganizationAbility organizationAbility,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
Get_Setup(organizationAbility, organizationUsers, sutProvider);
|
||||
|
||||
var customUser = organizationUsers.First();
|
||||
customUser.Type = OrganizationUserType.Custom;
|
||||
customUser.Permissions = CoreHelpers.ClassToJsonData(new Permissions
|
||||
{
|
||||
AccessReports = true,
|
||||
EditAssignedCollections = true,
|
||||
DeleteAssignedCollections = true,
|
||||
AccessEventLogs = true
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Get(organizationAbility.Id);
|
||||
|
||||
var customUserResponse = response.Data.First(r => r.Id == organizationUsers.First().Id);
|
||||
Assert.Equal(OrganizationUserType.Custom, customUserResponse.Type);
|
||||
Assert.True(customUserResponse.Permissions.AccessReports);
|
||||
Assert.True(customUserResponse.Permissions.AccessEventLogs);
|
||||
Assert.False(customUserResponse.Permissions.EditAssignedCollections);
|
||||
Assert.False(customUserResponse.Permissions.DeleteAssignedCollections);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Get_DowngradesCustomUsersWithDeprecatedPermissions(
|
||||
ICollection<OrganizationUserUserDetails> organizationUsers, OrganizationAbility organizationAbility,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
Get_Setup(organizationAbility, organizationUsers, sutProvider);
|
||||
|
||||
var customUser = organizationUsers.First();
|
||||
customUser.Type = OrganizationUserType.Custom;
|
||||
customUser.Permissions = CoreHelpers.ClassToJsonData(new Permissions
|
||||
{
|
||||
EditAssignedCollections = true,
|
||||
DeleteAssignedCollections = true,
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.Get(organizationAbility.Id);
|
||||
|
||||
var customUserResponse = response.Data.First(r => r.Id == organizationUsers.First().Id);
|
||||
Assert.Equal(OrganizationUserType.User, customUserResponse.Type);
|
||||
Assert.False(customUserResponse.Permissions.EditAssignedCollections);
|
||||
Assert.False(customUserResponse.Permissions.DeleteAssignedCollections);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetAccountRecoveryDetails_ReturnsDetails(
|
||||
@ -309,6 +245,8 @@ public class OrganizationUsersControllerTests
|
||||
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
|
||||
.Returns(organizationAbility);
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserUserDetailsQuery>().GetOrganizationUserUserDetails(Arg.Any<OrganizationUserUserDetailsQueryRequest>()).Returns(organizationUsers);
|
||||
|
||||
sutProvider.GetDependency<IAuthorizationService>().AuthorizeAsync(
|
||||
user: Arg.Any<ClaimsPrincipal>(),
|
||||
resource: Arg.Any<Object>(),
|
||||
|
@ -0,0 +1,117 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||
using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Api.Test.AdminConsole.Queries;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class OrganizationUserUserDetailsQueryTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Get_DowngradesCustomUsersWithDeprecatedPermissions(
|
||||
ICollection<OrganizationUserUserDetails> organizationUsers,
|
||||
SutProvider<OrganizationUserUserDetailsQuery> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
Get_Setup(organizationUsers, sutProvider, organizationId);
|
||||
|
||||
var customUser = organizationUsers.First();
|
||||
customUser.Type = OrganizationUserType.Custom;
|
||||
customUser.Permissions = CoreHelpers.ClassToJsonData(new Permissions
|
||||
{
|
||||
EditAssignedCollections = true,
|
||||
DeleteAssignedCollections = true,
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.GetOrganizationUserUserDetails(new OrganizationUserUserDetailsQueryRequest { OrganizationId = organizationId });
|
||||
|
||||
var customUserResponse = response.First(r => r.Id == organizationUsers.First().Id);
|
||||
Assert.Equal(OrganizationUserType.User, customUserResponse.Type);
|
||||
|
||||
var customUserPermissions = customUserResponse.GetPermissions();
|
||||
Assert.False(customUserPermissions.EditAssignedCollections);
|
||||
Assert.False(customUserPermissions.DeleteAssignedCollections);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Get_HandlesNullPermissionsObject(
|
||||
ICollection<OrganizationUserUserDetails> organizationUsers,
|
||||
SutProvider<OrganizationUserUserDetailsQuery> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
Get_Setup(organizationUsers, sutProvider, organizationId);
|
||||
organizationUsers.First().Permissions = "null";
|
||||
var response = await sutProvider.Sut.GetOrganizationUserUserDetails(new OrganizationUserUserDetailsQueryRequest { OrganizationId = organizationId });
|
||||
|
||||
Assert.True(response.All(r => organizationUsers.Any(ou => ou.Id == r.Id)));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Get_SetsDeprecatedCustomPermissionstoFalse(
|
||||
ICollection<OrganizationUserUserDetails> organizationUsers,
|
||||
SutProvider<OrganizationUserUserDetailsQuery> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
Get_Setup(organizationUsers, sutProvider, organizationId);
|
||||
|
||||
var customUser = organizationUsers.First();
|
||||
customUser.Type = OrganizationUserType.Custom;
|
||||
customUser.Permissions = CoreHelpers.ClassToJsonData(new Permissions
|
||||
{
|
||||
AccessReports = true,
|
||||
EditAssignedCollections = true,
|
||||
DeleteAssignedCollections = true,
|
||||
AccessEventLogs = true
|
||||
});
|
||||
|
||||
var response = await sutProvider.Sut.GetOrganizationUserUserDetails(new OrganizationUserUserDetailsQueryRequest { OrganizationId = organizationId });
|
||||
|
||||
var customUserResponse = response.First(r => r.Id == organizationUsers.First().Id);
|
||||
Assert.Equal(OrganizationUserType.Custom, customUserResponse.Type);
|
||||
|
||||
var customUserPermissions = customUserResponse.GetPermissions();
|
||||
Assert.True(customUserPermissions.AccessReports);
|
||||
Assert.True(customUserPermissions.AccessEventLogs);
|
||||
Assert.False(customUserPermissions.EditAssignedCollections);
|
||||
Assert.False(customUserPermissions.DeleteAssignedCollections);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Get_ReturnsUsers(
|
||||
ICollection<OrganizationUserUserDetails> organizationUsers,
|
||||
SutProvider<OrganizationUserUserDetailsQuery> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
Get_Setup(organizationUsers, sutProvider, organizationId);
|
||||
var response = await sutProvider.Sut.GetOrganizationUserUserDetails(new OrganizationUserUserDetailsQueryRequest { OrganizationId = organizationId });
|
||||
|
||||
Assert.True(response.All(r => organizationUsers.Any(ou => ou.Id == r.Id)));
|
||||
}
|
||||
|
||||
private void Get_Setup(
|
||||
ICollection<OrganizationUserUserDetails> organizationUsers,
|
||||
SutProvider<OrganizationUserUserDetailsQuery> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
foreach (var orgUser in organizationUsers)
|
||||
{
|
||||
orgUser.Permissions = null;
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||
.GetManyDetailsByOrganizationAsync(organizationId, Arg.Any<bool>(), Arg.Any<bool>())
|
||||
.Returns(organizationUsers);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user