diff --git a/src/Api/AdminConsole/Authorization/Requirements/ManageAccountRecoveryRequirement.cs b/src/Api/AdminConsole/Authorization/Requirements/ManageAccountRecoveryRequirement.cs new file mode 100644 index 0000000000..268fee5d95 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/Requirements/ManageAccountRecoveryRequirement.cs @@ -0,0 +1,20 @@ +#nullable enable + +using Bit.Core.Context; +using Bit.Core.Enums; + +namespace Bit.Api.AdminConsole.Authorization.Requirements; + +public class ManageAccountRecoveryRequirement : IOrganizationRequirement +{ + public async Task AuthorizeAsync( + CurrentContextOrganization? organizationClaims, + Func> isProviderUserForOrg) + => organizationClaims switch + { + { Type: OrganizationUserType.Owner } => true, + { Type: OrganizationUserType.Admin } => true, + { Permissions.ManageResetPassword: true } => true, + _ => await isProviderUserForOrg() + }; +} diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index e21dd3de49..536914b56f 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -1,4 +1,5 @@ -using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Api.AdminConsole.Authorization.Requirements; +using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; @@ -162,6 +163,12 @@ public class OrganizationUsersController : Controller [HttpGet("")] public async Task> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false) { + + if (_featureService.IsEnabled(FeatureFlagKeys.SeparateCustomRolePermissions)) + { + return await GetvNextAsync(orgId, includeGroups, includeCollections); + } + var authorized = (await _authorizationService.AuthorizeAsync( User, new OrganizationScope(orgId), OrganizationUserUserDetailsOperations.ReadAll)).Succeeded; if (!authorized) @@ -191,6 +198,37 @@ public class OrganizationUsersController : Controller return new ListResponseModel(responses); } + private async Task> GetvNextAsync(Guid orgId, bool includeGroups = false, bool includeCollections = false) + { + var request = new OrganizationUserUserDetailsQueryRequest + { + OrganizationId = orgId, + IncludeGroups = includeGroups, + IncludeCollections = includeCollections, + }; + + if ((await _authorizationService.AuthorizeAsync(User, new ManageUsersRequirement())).Succeeded) + { + return GetResultListResponseModel(await _organizationUserUserDetailsQuery.Get(request)); + } + + if ((await _authorizationService.AuthorizeAsync(User, new ManageAccountRecoveryRequirement())).Succeeded) + { + return GetResultListResponseModel(await _organizationUserUserDetailsQuery.GetAccountRecoveryEnrolledUsers(request)); + } + + throw new NotFoundException(); + } + + private ListResponseModel GetResultListResponseModel(IEnumerable<(OrganizationUserUserDetails OrgUser, + bool TwoFactorEnabled, bool ClaimedByOrganization)> results) + { + return new ListResponseModel(results + .Select(result => new OrganizationUserUserDetailsResponseModel(result)) + .ToList()); + } + + [HttpGet("{id}/groups")] public async Task> GetGroups(string orgId, string id) { diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs index 4e869f59b1..057841c7d2 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs @@ -126,6 +126,26 @@ public class OrganizationUserUserMiniDetailsResponseModel : ResponseModel public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel { + public OrganizationUserUserDetailsResponseModel((OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization) data, string obj = "organizationUserUserDetails") + : base(data.OrgUser, obj) + { + if (data.OrgUser == null) + { + throw new ArgumentNullException(nameof(data.OrgUser)); + } + + Name = data.OrgUser.Name; + Email = data.OrgUser.Email; + AvatarColor = data.OrgUser.AvatarColor; + TwoFactorEnabled = data.TwoFactorEnabled; + SsoBound = !string.IsNullOrWhiteSpace(data.OrgUser.SsoExternalId); + Collections = data.OrgUser.Collections.Select(c => new SelectionReadOnlyResponseModel(c)); + Groups = data.OrgUser.Groups; + // Prevent reset password when using key connector. + ResetPasswordEnrolled = ResetPasswordEnrolled && !data.OrgUser.UsesKeyConnector; + ClaimedByOrganization = data.ClaimedByOrganization; + } + public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser, bool twoFactorEnabled, bool claimedByOrganization, string obj = "organizationUserUserDetails") : base(organizationUser, obj) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IOrganizationUserUserDetailsQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IOrganizationUserUserDetailsQuery.cs index 8494a6d4ca..59162230da 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IOrganizationUserUserDetailsQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IOrganizationUserUserDetailsQuery.cs @@ -6,4 +6,8 @@ namespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; public interface IOrganizationUserUserDetailsQuery { Task> GetOrganizationUserUserDetails(OrganizationUserUserDetailsQueryRequest request); + + Task> Get(OrganizationUserUserDetailsQueryRequest request); + + Task> GetAccountRecoveryEnrolledUsers(OrganizationUserUserDetailsQueryRequest request); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs index 22fce08021..587e04826b 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs @@ -1,5 +1,9 @@ -using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Core.Utilities; using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; @@ -9,12 +13,21 @@ namespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers; public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuery { private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IFeatureService _featureService; + private readonly IGetOrganizationUsersClaimedStatusQuery _getOrganizationUsersClaimedStatusQuery; public OrganizationUserUserDetailsQuery( - IOrganizationUserRepository organizationUserRepository + IOrganizationUserRepository organizationUserRepository, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + IFeatureService featureService, + IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery ) { _organizationUserRepository = organizationUserRepository; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _featureService = featureService; + _getOrganizationUsersClaimedStatusQuery = getOrganizationUsersClaimedStatusQuery; } /// @@ -37,4 +50,42 @@ public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuer return o; }); } + + /// + /// Get the organization user user details, two factor enabled status, and + /// claimed status for the provided request. + /// + /// Request details for the query + /// List of OrganizationUserUserDetails + public async Task> Get(OrganizationUserUserDetailsQueryRequest request) + { + var organizationUsers = await GetOrganizationUserUserDetails(request); + + var organizationUsersTwoFactorEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers)).ToDictionary(u => u.user.Id); + var organizationUsersClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(request.OrganizationId, organizationUsers.Select(o => o.Id)); + var responses = organizationUsers.Select(o => (o, organizationUsersTwoFactorEnabled[o.Id].twoFactorIsEnabled, organizationUsersClaimedStatus[o.Id])); + + + return responses; + } + + /// + /// Get the organization users user details, two factor enabled status, and + /// claimed status for confirmed users that are enrolled in account recovery + /// + /// Request details for the query + /// List of OrganizationUserUserDetails + public async Task> GetAccountRecoveryEnrolledUsers(OrganizationUserUserDetailsQueryRequest request) + { + var organizationUsers = (await GetOrganizationUserUserDetails(request)) + .Where(o => o.Status.Equals(OrganizationUserStatusType.Confirmed) && o.UsesKeyConnector == false && !String.IsNullOrEmpty(o.ResetPasswordKey)); + + var organizationUsersTwoFactorEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers)).ToDictionary(u => u.user.Id); + var organizationUsersClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(request.OrganizationId, organizationUsers.Select(o => o.Id)); + var responses = organizationUsers + .Select(o => (o, organizationUsersTwoFactorEnabled[o.Id].twoFactorIsEnabled, organizationUsersClaimedStatus[o.Id])); + + return responses; + } + } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 13d0bad495..16b9849451 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -205,6 +205,7 @@ public static class FeatureFlagKeys public const string DesktopCipherForms = "pm-18520-desktop-cipher-forms"; public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk"; public const string EndUserNotifications = "pm-10609-end-user-notifications"; + public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions"; public const string PhishingDetection = "phishing-detection"; public static List GetAllKeys()