Implement feature flag for fetching new policies and organization details in SyncController (#7506) (#7529)

- Added support for retrieving confirmed accepted policies and organization user details based on the feature flag 'PoliciesInAcceptedState'.
- Updated SyncResponseModel to include new properties for these details.
- Enhanced SyncControllerTests to verify behavior with the feature flag enabled and disabled.
This commit is contained in:
Jared
2026-04-30 15:52:10 -04:00
committed by GitHub
parent 28bd2862b8
commit cdfb54e71b
3 changed files with 108 additions and 2 deletions

View File

@@ -16,6 +16,7 @@ using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@@ -135,9 +136,18 @@ public class SyncController : Controller
userAccountKeys = await _userAccountKeysQuery.Run(user);
}
IEnumerable<Policy> policiesNew = null;
IEnumerable<OrganizationUserOrganizationDetails> organizationUserDetailsNew = null;
if (_featureService.IsEnabled(FeatureFlagKeys.PoliciesInAcceptedState))
{
policiesNew = await _policyRepository.GetManyConfirmedAcceptedByUserIdAsync(user.Id);
organizationUserDetailsNew = await _organizationUserRepository.GetManyConfirmedAcceptedDetailsByUserAsync(user.Id);
}
var response = new SyncResponseModel(_globalSettings, user, userAccountKeys, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,
organizationIdsClaimingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends, webAuthnCredentials);
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends, webAuthnCredentials,
policiesNew, organizationUserDetailsNew);
return response;
}

View File

@@ -44,7 +44,9 @@ public class SyncResponseModel() : ResponseModel("sync")
bool excludeDomains,
IEnumerable<Policy> policies,
IEnumerable<Send> sends,
IEnumerable<WebAuthnCredential> webAuthnCredentials)
IEnumerable<WebAuthnCredential> webAuthnCredentials,
IEnumerable<Policy> policiesNew = null,
IEnumerable<OrganizationUserOrganizationDetails> organizationUserDetailsNew = null)
: this()
{
Profile = new ProfileResponseModel(user, userAccountKeysData, organizationUserDetails, providerUserDetails,
@@ -61,6 +63,8 @@ public class SyncResponseModel() : ResponseModel("sync")
c => new CollectionDetailsResponseModel(c)) ?? new List<CollectionDetailsResponseModel>();
Domains = excludeDomains ? null : new DomainsResponseModel(user, false);
Policies = policies?.Select(p => new PolicyResponseModel(p)) ?? new List<PolicyResponseModel>();
PoliciesNew = policiesNew?.Select(p => new PolicyResponseModel(p));
OrganizationsNew = organizationUserDetailsNew?.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsClaimingingUser));
Sends = sends.Select(s => new SendResponseModel(s));
var webAuthnPrfOptions = webAuthnCredentials
.Where(c => c.GetPrfStatus() == WebAuthnPrfStatus.Enabled)
@@ -119,6 +123,18 @@ public class SyncResponseModel() : ResponseModel("sync")
public IEnumerable<CipherDetailsResponseModel> Ciphers { get; set; }
public DomainsResponseModel Domains { get; set; }
public IEnumerable<PolicyResponseModel> Policies { get; set; }
/// <summary>
/// Policies for organizations where the user is in the Confirmed or Accepted status.
/// Null when the <c>pm-34145-policies-in-accepted-state</c> feature flag is disabled.
/// New clients should prefer this property and fall back to <see cref="Policies"/> if absent.
/// </summary>
public IEnumerable<PolicyResponseModel> PoliciesNew { get; set; }
/// <summary>
/// Organizations where the user is in the Confirmed or Accepted status.
/// Null when the <c>pm-34145-policies-in-accepted-state</c> feature flag is disabled.
/// New clients should prefer this property and fall back to <see cref="Profile"/>.<c>Organizations</c> if absent.
/// </summary>
public IEnumerable<ProfileOrganizationResponseModel> OrganizationsNew { get; set; }
public IEnumerable<SendResponseModel> Sends { get; set; }
public UserDecryptionResponseModel UserDecryption { get; set; }
}

View File

@@ -615,6 +615,86 @@ public class SyncControllerTests
Assert.Contains(result.Ciphers, c => c.Type == CipherType.Login);
}
[Theory]
[BitAutoData]
public async Task Get_PoliciesInAcceptedState_FlagEnabled_CallsNewRepositoryMethods(
User user,
ICollection<Policy> policiesAccepted,
ICollection<OrganizationUserOrganizationDetails> organizationsAccepted,
SutProvider<SyncController> sutProvider)
{
user.EquivalentDomains = null;
user.ExcludedGlobalEquivalentDomains = null;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
SignatureKeyPairData = null,
});
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PoliciesInAcceptedState).Returns(true);
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
policyRepository.GetManyConfirmedAcceptedByUserIdAsync(user.Id).Returns(policiesAccepted);
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
organizationUserRepository.GetManyConfirmedAcceptedDetailsByUserAsync(user.Id).Returns(organizationsAccepted);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(user).Returns(false);
userService.HasPremiumFromOrganization(user).Returns(false);
var result = await sutProvider.Sut.Get();
Assert.IsType<SyncResponseModel>(result);
await policyRepository.Received(1).GetManyConfirmedAcceptedByUserIdAsync(user.Id);
await organizationUserRepository.Received(1).GetManyConfirmedAcceptedDetailsByUserAsync(user.Id);
Assert.NotNull(result.PoliciesNew);
Assert.NotNull(result.OrganizationsNew);
}
[Theory]
[BitAutoData]
public async Task Get_PoliciesInAcceptedState_FlagDisabled_DoesNotCallNewRepositoryMethods(
User user,
SutProvider<SyncController> sutProvider)
{
user.EquivalentDomains = null;
user.ExcludedGlobalEquivalentDomains = null;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
SignatureKeyPairData = null,
});
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PoliciesInAcceptedState).Returns(false);
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(user).Returns(false);
userService.HasPremiumFromOrganization(user).Returns(false);
var result = await sutProvider.Sut.Get();
Assert.IsType<SyncResponseModel>(result);
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
await policyRepository.DidNotReceive().GetManyConfirmedAcceptedByUserIdAsync(Arg.Any<Guid>());
await organizationUserRepository.DidNotReceive().GetManyConfirmedAcceptedDetailsByUserAsync(Arg.Any<Guid>());
Assert.Null(result.PoliciesNew);
Assert.Null(result.OrganizationsNew);
}
private async Task AssertMethodsCalledAsync(IUserService userService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IOrganizationUserRepository organizationUserRepository,