1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-03 17:12:49 -05:00

[AC-2027] Update Flexible Collections logic to use organization property (#3644)

* Update optionality to use org.FlexibleCollections

Also break old feature flag key to ensure it's never enabled

* Add logic to set defaults for collection management setting

* Update optionality logic to use org property

* Add comments

* Add helper method for getting individual orgAbility

* Fix validate user update permissions interface

* Fix tests

* dotnet format

* Fix more tests

* Simplify self-hosted update logic

* Fix mapping

* Use new getOrganizationAbility method

* Refactor invite and save orgUser methods

Pass in whole organization object instead of using OrganizationAbility

* fix CipherService tests

* dotnet format

* Remove manager check to simplify this set of changes

* Misc cleanup before review

* Fix undefined variable

* Refactor bulk-access endpoint to avoid early repo call

* Restore manager check

* Add tests for UpdateOrganizationLicenseCommand

* Add nullable regions

* Delete unused dependency

* dotnet format

* Fix test
This commit is contained in:
Thomas Rittson
2024-01-17 22:33:35 +10:00
committed by GitHub
parent ef37cdc71a
commit 96f9fbb951
27 changed files with 472 additions and 411 deletions

View File

@ -241,11 +241,13 @@ public class Organization : ITableObject<Guid>, ISubscriber, IStorable, IStorabl
return providers[provider];
}
public void UpdateFromLicense(
OrganizationLicense license,
bool flexibleCollectionsMvpIsEnabled,
bool flexibleCollectionsV1IsEnabled)
public void UpdateFromLicense(OrganizationLicense license)
{
// The following properties are intentionally excluded from being updated:
// - Id - self-hosted org will have its own unique Guid
// - MaxStorageGb - not enforced for self-hosted because we're not providing the storage
// - FlexibleCollections - the self-hosted organization must do its own data migration to set this property, it cannot be updated from cloud
Name = license.Name;
BusinessName = license.BusinessName;
BillingEmail = license.BillingEmail;
@ -275,7 +277,7 @@ public class Organization : ITableObject<Guid>, ISubscriber, IStorable, IStorabl
UseSecretsManager = license.UseSecretsManager;
SmSeats = license.SmSeats;
SmServiceAccounts = license.SmServiceAccounts;
LimitCollectionCreationDeletion = !flexibleCollectionsMvpIsEnabled || license.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = !flexibleCollectionsV1IsEnabled || license.AllowAdminAccessToAllCollectionItems;
LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems;
}
}

View File

@ -145,7 +145,8 @@ public class SelfHostedOrganizationDetails : Organization
MaxAutoscaleSeats = MaxAutoscaleSeats,
OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,
LimitCollectionCreationDeletion = LimitCollectionCreationDeletion,
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
FlexibleCollections = FlexibleCollections
};
}
}

View File

@ -65,8 +65,6 @@ public class OrganizationService : IOrganizationService
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IFeatureService _featureService;
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
public OrganizationService(
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
@ -418,6 +416,9 @@ public class OrganizationService : IOrganizationService
}
}
/// <summary>
/// Create a new organization in a cloud environment
/// </summary>
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(OrganizationSignup signup,
bool provider = false)
{
@ -440,8 +441,9 @@ public class OrganizationService : IOrganizationService
await ValidateSignUpPoliciesAsync(signup.Owner.Id);
}
var flexibleCollectionsIsEnabled =
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
var flexibleCollectionsSignupEnabled =
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsSignup, _currentContext);
var flexibleCollectionsV1IsEnabled =
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1, _currentContext);
@ -482,7 +484,15 @@ public class OrganizationService : IOrganizationService
Status = OrganizationStatusType.Created,
UsePasswordManager = true,
UseSecretsManager = signup.UseSecretsManager,
LimitCollectionCreationDeletion = !flexibleCollectionsIsEnabled,
// This feature flag indicates that new organizations should be automatically onboarded to
// Flexible Collections enhancements
FlexibleCollections = flexibleCollectionsSignupEnabled,
// These collection management settings smooth the migration for existing organizations by disabling some FC behavior.
// If the organization is onboarded to Flexible Collections on signup, we turn them OFF to enable all new behaviour.
// If the organization is NOT onboarded now, they will have to be migrated later, so they default to ON to limit FC changes on migration.
LimitCollectionCreationDeletion = !flexibleCollectionsSignupEnabled,
AllowAdminAccessToAllCollectionItems = !flexibleCollectionsV1IsEnabled
};
@ -534,6 +544,9 @@ public class OrganizationService : IOrganizationService
}
}
/// <summary>
/// Create a new organization on a self-hosted instance
/// </summary>
public async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(
OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey,
string privateKey)
@ -558,10 +571,8 @@ public class OrganizationService : IOrganizationService
await ValidateSignUpPoliciesAsync(owner.Id);
var flexibleCollectionsMvpIsEnabled =
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
var flexibleCollectionsV1IsEnabled =
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1, _currentContext);
var flexibleCollectionsSignupEnabled =
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsSignup, _currentContext);
var organization = new Organization
{
@ -603,8 +614,12 @@ public class OrganizationService : IOrganizationService
UseSecretsManager = license.UseSecretsManager,
SmSeats = license.SmSeats,
SmServiceAccounts = license.SmServiceAccounts,
LimitCollectionCreationDeletion = !flexibleCollectionsMvpIsEnabled || license.LimitCollectionCreationDeletion,
AllowAdminAccessToAllCollectionItems = !flexibleCollectionsV1IsEnabled || license.AllowAdminAccessToAllCollectionItems
LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion,
AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems,
// This feature flag indicates that new organizations should be automatically onboarded to
// Flexible Collections enhancements
FlexibleCollections = flexibleCollectionsSignupEnabled,
};
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
@ -616,6 +631,10 @@ public class OrganizationService : IOrganizationService
return result;
}
/// <summary>
/// Private helper method to create a new organization.
/// This is common code used by both the cloud and self-hosted methods.
/// </summary>
private async Task<Tuple<Organization, OrganizationUser>> SignUpAsync(Organization organization,
Guid ownerId, string ownerKey, string collectionName, bool withPayment)
{
@ -829,6 +848,7 @@ public class OrganizationService : IOrganizationService
{
var inviteTypes = new HashSet<OrganizationUserType>(invites.Where(i => i.invite.Type.HasValue)
.Select(i => i.invite.Type.Value));
if (invitingUserId.HasValue && inviteTypes.Count > 0)
{
foreach (var (invite, _) in invites)
@ -2008,7 +2028,11 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("Custom users can only grant the same custom permissions that they have.");
}
if (FlexibleCollectionsIsEnabled && newType == OrganizationUserType.Manager && oldType is not OrganizationUserType.Manager)
// TODO: pass in the whole organization object when this is refactored into a command/query
// See AC-2036
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId);
var flexibleCollectionsEnabled = organizationAbility?.FlexibleCollections ?? false;
if (flexibleCollectionsEnabled && newType == OrganizationUserType.Manager && oldType is not OrganizationUserType.Manager)
{
throw new BadRequestException("Manager role is deprecated after Flexible Collections.");
}

View File

@ -96,12 +96,17 @@ public static class FeatureFlagKeys
public const string VaultOnboarding = "vault-onboarding";
public const string AutofillV2 = "autofill-v2";
public const string BrowserFilelessImport = "browser-fileless-import";
public const string FlexibleCollections = "flexible-collections";
/// <summary>
/// Deprecated - never used, do not use. Will always default to false. Will be deleted as part of Flexible Collections cleanup
/// </summary>
public const string FlexibleCollections = "flexible-collections-disabled-do-not-use";
public const string FlexibleCollectionsV1 = "flexible-collections-v-1"; // v-1 is intentional
public const string BulkCollectionAccess = "bulk-collection-access";
public const string AutofillOverlay = "autofill-overlay";
public const string ItemShare = "item-share";
public const string KeyRotationImprovements = "key-rotation-improvements";
public const string FlexibleCollectionsMigration = "flexible-collections-migration";
public const string FlexibleCollectionsSignup = "flexible-collections-signup";
public static List<string> GetAllKeys()
{

View File

@ -5,7 +5,6 @@ using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Identity;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
@ -26,8 +25,6 @@ public class CurrentContext : ICurrentContext
private IEnumerable<ProviderOrganizationProviderDetails> _providerOrganizationProviderDetails;
private IEnumerable<ProviderUserOrganizationDetails> _providerUserOrganizations;
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, this);
public virtual HttpContext HttpContext { get; set; }
public virtual Guid? UserId { get; set; }
public virtual User User { get; set; }
@ -283,11 +280,6 @@ public class CurrentContext : ICurrentContext
public async Task<bool> OrganizationManager(Guid orgId)
{
if (FlexibleCollectionsIsEnabled)
{
throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF.");
}
return await OrganizationAdmin(orgId) ||
(Organizations?.Any(o => o.Id == orgId && o.Type == OrganizationUserType.Manager) ?? false);
}
@ -350,22 +342,12 @@ public class CurrentContext : ICurrentContext
public async Task<bool> EditAssignedCollections(Guid orgId)
{
if (FlexibleCollectionsIsEnabled)
{
throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF.");
}
return await OrganizationManager(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.EditAssignedCollections ?? false)) ?? false);
}
public async Task<bool> DeleteAssignedCollections(Guid orgId)
{
if (FlexibleCollectionsIsEnabled)
{
throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF.");
}
return await OrganizationManager(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.DeleteAssignedCollections ?? false)) ?? false);
}
@ -378,11 +360,6 @@ public class CurrentContext : ICurrentContext
* This entire method will be moved to the CollectionAuthorizationHandler in the future
*/
if (FlexibleCollectionsIsEnabled)
{
throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF.");
}
var org = GetOrganization(orgId);
return await EditAssignedCollections(orgId)
|| await DeleteAssignedCollections(orgId)

View File

@ -2,7 +2,6 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations;
@ -18,21 +17,15 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman
private readonly ILicensingService _licensingService;
private readonly IGlobalSettings _globalSettings;
private readonly IOrganizationService _organizationService;
private readonly IFeatureService _featureService;
private readonly ICurrentContext _currentContext;
public UpdateOrganizationLicenseCommand(
ILicensingService licensingService,
IGlobalSettings globalSettings,
IOrganizationService organizationService,
IFeatureService featureService,
ICurrentContext currentContext)
IOrganizationService organizationService)
{
_licensingService = licensingService;
_globalSettings = globalSettings;
_organizationService = organizationService;
_featureService = featureService;
_currentContext = currentContext;
}
public async Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization,
@ -65,10 +58,8 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman
private async Task UpdateOrganizationAsync(SelfHostedOrganizationDetails selfHostedOrganizationDetails, OrganizationLicense license)
{
var flexibleCollectionsMvpIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
var flexibleCollectionsV1IsEnabled = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1, _currentContext);
var organization = selfHostedOrganizationDetails.ToOrganization();
organization.UpdateFromLicense(license, flexibleCollectionsMvpIsEnabled, flexibleCollectionsV1IsEnabled);
organization.UpdateFromLicense(license);
await _organizationService.ReplaceAndUpdateCacheAsync(organization);
}

View File

@ -8,6 +8,9 @@ namespace Bit.Core.Services;
public interface IApplicationCacheService
{
Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync();
#nullable enable
Task<OrganizationAbility?> GetOrganizationAbilityAsync(Guid orgId);
#nullable disable
Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync();
Task UpsertOrganizationAbilityAsync(Organization organization);
Task UpsertProviderAbilityAsync(Provider provider);

View File

@ -30,6 +30,15 @@ public class InMemoryApplicationCacheService : IApplicationCacheService
return _orgAbilities;
}
#nullable enable
public async Task<OrganizationAbility?> GetOrganizationAbilityAsync(Guid organizationId)
{
(await GetOrganizationAbilitiesAsync())
.TryGetValue(organizationId, out var organizationAbility);
return organizationAbility;
}
#nullable disable
public virtual async Task<IDictionary<Guid, ProviderAbility>> GetProviderAbilitiesAsync()
{
await InitProviderAbilitiesAsync();

View File

@ -788,7 +788,7 @@ public class CipherService : ICipherService
{
collection.SetNewId();
newCollections.Add(collection);
if (UseFlexibleCollections)
if (org.FlexibleCollections)
{
newCollectionUsers.Add(new CollectionUser
{