From 3f629e0a5ae68681ead65a7e501d00894c902841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:21:51 +0100 Subject: [PATCH] [PM-11334] Add managed status to sync data (#4791) * Refactor UserService to add GetOrganizationManagingUserAsync method to retrive the organization that manages a user * Refactor SyncController and AccountsController to include ManagedByOrganizationId in profile response --- .../Auth/Controllers/AccountsController.cs | 35 ++++++++++++++++--- .../Models/Response/ProfileResponseModel.cs | 5 ++- src/Api/Vault/Controllers/SyncController.cs | 27 +++++++++++--- .../Models/Response/SyncResponseModel.cs | 3 +- src/Core/Services/IUserService.cs | 7 ++++ .../Services/Implementations/UserService.cs | 9 ++++- 6 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 3370b8939..cf74460fc 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -443,10 +443,11 @@ public class AccountsController : Controller var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); + var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user); var response = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, twoFactorEnabled, - hasPremiumFromOrg); + hasPremiumFromOrg, managedByOrganizationId); return response; } @@ -471,7 +472,12 @@ public class AccountsController : Controller } await _userService.SaveUserAsync(model.ToUser(user)); - var response = new ProfileResponseModel(user, null, null, null, await _userService.TwoFactorIsEnabledAsync(user), await _userService.HasPremiumFromOrganization(user)); + + var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); + var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user); + + var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, managedByOrganizationId); return response; } @@ -485,7 +491,12 @@ public class AccountsController : Controller throw new UnauthorizedAccessException(); } await _userService.SaveUserAsync(model.ToUser(user), true); - var response = new ProfileResponseModel(user, null, null, null, await _userService.TwoFactorIsEnabledAsync(user), await _userService.HasPremiumFromOrganization(user)); + + var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); + var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user); + + var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId); return response; } @@ -633,7 +644,12 @@ public class AccountsController : Controller BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode, }); - var profile = new ProfileResponseModel(user, null, null, null, await _userService.TwoFactorIsEnabledAsync(user), await _userService.HasPremiumFromOrganization(user)); + + var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); + var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user); + + var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId); return new PaymentResponseModel { UserProfile = profile, @@ -920,4 +936,15 @@ public class AccountsController : Controller throw new BadRequestException("Token", "Invalid token"); } } + + private async Task GetManagedByOrganizationIdAsync(User user) + { + if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) + { + return null; + } + + var organizationManagingUser = await _userService.GetOrganizationManagingUserAsync(user.Id); + return organizationManagingUser?.Id; + } } diff --git a/src/Api/Models/Response/ProfileResponseModel.cs b/src/Api/Models/Response/ProfileResponseModel.cs index fbb60e718..f5d0382e5 100644 --- a/src/Api/Models/Response/ProfileResponseModel.cs +++ b/src/Api/Models/Response/ProfileResponseModel.cs @@ -14,7 +14,8 @@ public class ProfileResponseModel : ResponseModel IEnumerable providerUserDetails, IEnumerable providerUserOrganizationDetails, bool twoFactorEnabled, - bool premiumFromOrganization) : base("profile") + bool premiumFromOrganization, + Guid? managedByOrganizationId) : base("profile") { if (user == null) { @@ -40,6 +41,7 @@ public class ProfileResponseModel : ResponseModel Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p)); ProviderOrganizations = providerUserOrganizationDetails?.Select(po => new ProfileProviderOrganizationResponseModel(po)); + ManagedByOrganizationId = managedByOrganizationId; } public ProfileResponseModel() : base("profile") @@ -61,6 +63,7 @@ public class ProfileResponseModel : ResponseModel public bool UsesKeyConnector { get; set; } public string AvatarColor { get; set; } public DateTime CreationDate { get; set; } + public Guid? ManagedByOrganizationId { get; set; } public IEnumerable Organizations { get; set; } public IEnumerable Providers { get; set; } public IEnumerable ProviderOrganizations { get; set; } diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index 0381bdca6..79c71bb87 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -1,4 +1,5 @@ using Bit.Api.Vault.Models.Response; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; @@ -6,6 +7,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -30,6 +32,7 @@ public class SyncController : Controller private readonly IPolicyRepository _policyRepository; private readonly ISendRepository _sendRepository; private readonly GlobalSettings _globalSettings; + private readonly IFeatureService _featureService; public SyncController( IUserService userService, @@ -41,7 +44,8 @@ public class SyncController : Controller IProviderUserRepository providerUserRepository, IPolicyRepository policyRepository, ISendRepository sendRepository, - GlobalSettings globalSettings) + GlobalSettings globalSettings, + IFeatureService featureService) { _userService = userService; _folderRepository = folderRepository; @@ -53,6 +57,7 @@ public class SyncController : Controller _policyRepository = policyRepository; _sendRepository = sendRepository; _globalSettings = globalSettings; + _featureService = featureService; } [HttpGet("")] @@ -90,9 +95,23 @@ public class SyncController : Controller var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); - var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationUserDetails, - providerUserDetails, providerUserOrganizationDetails, folders, collections, ciphers, - collectionCiphersGroupDict, excludeDomains, policies, sends); + var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user, organizationUserDetails); + + var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, + managedByOrganizationId, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, + folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends); return response; } + + private async Task GetManagedByOrganizationIdAsync(User user, IEnumerable organizationUserDetails) + { + if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) || + !organizationUserDetails.Any(o => o.Enabled && o.UseSso)) + { + return null; + } + + var organizationManagingUser = await _userService.GetOrganizationManagingUserAsync(user.Id); + return organizationManagingUser?.Id; + } } diff --git a/src/Api/Vault/Models/Response/SyncResponseModel.cs b/src/Api/Vault/Models/Response/SyncResponseModel.cs index ca833738a..2170a5232 100644 --- a/src/Api/Vault/Models/Response/SyncResponseModel.cs +++ b/src/Api/Vault/Models/Response/SyncResponseModel.cs @@ -21,6 +21,7 @@ public class SyncResponseModel : ResponseModel User user, bool userTwoFactorEnabled, bool userHasPremiumFromOrganization, + Guid? managedByOrganizationId, IEnumerable organizationUserDetails, IEnumerable providerUserDetails, IEnumerable providerUserOrganizationDetails, @@ -34,7 +35,7 @@ public class SyncResponseModel : ResponseModel : base("sync") { Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails, - providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization); + providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId); Folders = folders.Select(f => new FolderResponseModel(f)); Ciphers = ciphers.Select(c => new CipherDetailsResponseModel(c, globalSettings, collectionCiphersDict)); Collections = collections?.Select( diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index f3ada234a..0135b5f1b 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -1,4 +1,5 @@ using System.Security.Claims; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Entities; @@ -95,4 +96,10 @@ public interface IUserService /// The organization must be enabled and be on an Enterprise plan. /// Task IsManagedByAnyOrganizationAsync(Guid userId); + + /// + /// Gets the organization that manages the user. + /// + /// + Task GetOrganizationManagingUserAsync(Guid userId); } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 46f48ef26..87fdd75fe 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1,4 +1,5 @@ using System.Security.Claims; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; @@ -1245,13 +1246,19 @@ public class UserService : UserManager, IUserService, IDisposable } public async Task IsManagedByAnyOrganizationAsync(Guid userId) + { + var managingOrganization = await GetOrganizationManagingUserAsync(userId); + return managingOrganization != null; + } + + public async Task GetOrganizationManagingUserAsync(Guid userId) { // Users can only be managed by an Organization that is enabled and can have organization domains var organization = await _organizationRepository.GetByClaimedUserDomainAsync(userId); // TODO: Replace "UseSso" with a new organization ability like "UseOrganizationDomains" (PM-11622). // Verified domains were tied to SSO, so we currently check the "UseSso" organization ability. - return organization is { Enabled: true, UseSso: true }; + return (organization is { Enabled: true, UseSso: true }) ? organization : null; } ///