mirror of
https://github.com/bitwarden/server.git
synced 2025-07-03 00:52:49 -05:00
[AC-1373] Flexible Collections (#3245)
* [AC-1117] Add manage permission (#3126) * Update sql files to add Manage permission * Add migration script * Rename collection manage migration file to remove duplicate migration date * Migrations * Add manage to models * Add manage to repository * Add constraint to Manage columns * Migration lint fixes * Add manage to OrganizationUserUserDetails_ReadWithCollectionsById * Add missing manage fields * Add 'Manage' to UserCollectionDetails * Use CREATE OR ALTER where possible * [AC-1374] Limit collection creation/deletion to Owner/Admin (#3145) * feat: update org table with new column, write migration, refs AC-1374 * feat: update views with new column, refs AC-1374 * feat: Alter sprocs (org create/update) to include new column, refs AC-1374 * feat: update entity/data/request/response models to handle new column, refs AC-1374 * feat: update necessary Provider related views during migration, refs AC-1374 * fix: update org create to default new column to false, refs AC-1374 * feat: added new API/request model for collection management and removed property from update request model, refs AC-1374 * fix: renamed migration script to be after secrets manage beta column changes, refs AC-1374 * fix: dotnet format, refs AC-1374 * feat: add ef migrations to reflect mssql changes, refs AC-1374 * fix: dotnet format, refs AC-1374 * feat: update API signature to accept Guid and explain Cd verbiage, refs AC-1374 * fix: merge conflict resolution * [AC-1174] CollectionUser and CollectionGroup authorization handlers (#3194) * [AC-1174] Introduce BulkAuthorizationHandler.cs * [AC-1174] Introduce CollectionUserAuthorizationHandler * [AC-1174] Add CreateForNewCollection CollectionUser requirement * [AC-1174] Add some more details to CollectionCustomization * [AC-1174] Formatting * [AC-1174] Add CollectionGroupOperation.cs * [AC-1174] Introduce CollectionGroupAuthorizationHandler.cs * [AC-1174] Cleanup CollectionFixture customization Implement and use re-usable extension method to support seeded Guids * [AC-1174] Introduce WithValueFromList AutoFixtureExtensions Modify CollectionCustomization to use multiple organization Ids for auto generated test data * [AC-1174] Simplify CollectionUserAuthorizationHandler.cs Modify the authorization handler to only perform authorization logic. Validation logic will need to be handled by any calling commands/controllers instead. * [AC-1174] Introduce shared CollectionAccessAuthorizationHandlerBase A shared base authorization handler was created for both CollectionUser and CollectionGroup resources, as they share the same underlying management authorization logic. * [AC-1174] Update CollectionUserAuthorizationHandler and CollectionGroupAuthorizationHandler to use the new CollectionAccessAuthorizationHandlerBase class * [AC-1174] Formatting * [AC-1174] Cleanup typo and redundant ToList() call * [AC-1174] Add check for provider users * [AC-1174] Reduce nested loops * [AC-1174] Introduce ICollectionAccess.cs * [AC-1174] Remove individual CollectionGroup and CollectionUser auth handlers and use base class instead * [AC-1174] Tweak unit test to fail minimally * [AC-1174] Reorganize authorization handlers in Core project * [AC-1174] Introduce new AddCoreAuthorizationHandlers() extension method * [AC-1174] Move CollectionAccessAuthorizationHandler into Api project * [AC-1174] Move CollectionFixture to Vault folder * [AC-1174] Rename operation to CreateUpdateDelete * [AC-1174] Require single organization for collection access authorization handler - Add requirement that all target collections must belong to the same organization - Simplify logic related to multiple organizations - Update tests and helpers - Use ToHashSet to improve lookup time * [AC-1174] Fix null reference exception * [AC-1174] Throw bad request exception when collections belong to different organizations * [AC-1174] Switch to CollectionAuthorizationHandler instead of CollectionAccessAuthorizationHandler to reduce complexity * Fix improper merge conflict resolution * fix: add permission check for collection management api, refs AC-1647 (#3252) * [AC-1125] Enforce org setting for creating/deleting collections (#3241) * [AC-1117] Add manage permission (#3126) * Update sql files to add Manage permission * Add migration script * Rename collection manage migration file to remove duplicate migration date * Migrations * Add manage to models * Add manage to repository * Add constraint to Manage columns * Migration lint fixes * Add manage to OrganizationUserUserDetails_ReadWithCollectionsById * Add missing manage fields * Add 'Manage' to UserCollectionDetails * Use CREATE OR ALTER where possible * [AC-1374] Limit collection creation/deletion to Owner/Admin (#3145) * feat: update org table with new column, write migration, refs AC-1374 * feat: update views with new column, refs AC-1374 * feat: Alter sprocs (org create/update) to include new column, refs AC-1374 * feat: update entity/data/request/response models to handle new column, refs AC-1374 * feat: update necessary Provider related views during migration, refs AC-1374 * fix: update org create to default new column to false, refs AC-1374 * feat: added new API/request model for collection management and removed property from update request model, refs AC-1374 * fix: renamed migration script to be after secrets manage beta column changes, refs AC-1374 * fix: dotnet format, refs AC-1374 * feat: add ef migrations to reflect mssql changes, refs AC-1374 * fix: dotnet format, refs AC-1374 * feat: update API signature to accept Guid and explain Cd verbiage, refs AC-1374 * feat: created collection auth handler/operations, added LimitCollectionCdOwnerAdmin to CurrentContentOrganization, refs AC-1125 * feat: create vault service collection extensions and register with base services, refs AC-1125 * feat: deprecated CurrentContext.CreateNewCollections, refs AC-1125 * feat: deprecate DeleteAnyCollection for single resource usages, refs AC-1125 * feat: move service registration to api, update references, refs AC-1125 * feat: add bulk delete authorization handler, refs AC-1125 * feat: always assign user and give manage access on create, refs AC-1125 * fix: updated CurrentContextOrganization type, refs AC-1125 * feat: combined existing collection authorization handlers/operations, refs AC-1125 * fix: OrganizationServiceTests -> CurrentContentOrganization typo, refs AC-1125 * fix: format, refs AC-1125 * fix: update collection controller tests, refs AC-1125 * fix: dotnet format, refs AC-1125 * feat: removed extra BulkAuthorizationHandler, refs AC-1125 * fix: dotnet format, refs AC-1125 * fix: change string to guid for org id, update bulk delete request model, refs AC-1125 * fix: remove delete many collection check, refs AC-1125 * fix: clean up collection auth handler, refs AC-1125 * fix: format fix for CollectionOperations, refs AC-1125 * fix: removed unnecessary owner check, add org null check to custom permission validation, refs AC-1125 * fix: remove unused methods in CurrentContext, refs AC-1125 * fix: removed obsolete test, fixed failling delete many test, refs AC-1125 * fix: CollectionAuthorizationHandlerTests fixes, refs AC-1125 * fix: OrganizationServiceTests fix broken test by mocking GetOrganization, refs AC-1125 * fix: CollectionAuthorizationHandler - remove unused repository, refs AC-1125 * feat: moved UserId null check to common method, refs AC-1125 * fix: updated auth handler tests to remove dependency on requirement for common code checks, refs AC-1125 * feat: updated conditionals/comments for create/delete methods within colleciton auth handler, refs AC-1125 * feat: added create/delete collection auth handler success methods, refs AC-1125 * fix: new up permissions to prevent excessive null checks, refs AC-1125 * fix: remove old reference to CreateNewCollections, refs AC-1125 * fix: typo within ViewAssignedCollections method, refs AC-1125 --------- Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com> * refactor: remove organizationId from CollectionBulkDeleteRequestModel, refs AC-1649 (#3282) * [AC-1174] Bulk Collection Management (#3229) * [AC-1174] Update SelectionReadOnlyRequestModel to use Guid for Id property * [AC-1174] Introduce initial bulk-access collection endpoint * [AC-1174] Introduce BulkAddCollectionAccessCommand and validation logic/tests * [AC-1174] Add CreateOrUpdateAccessMany method to CollectionRepository * [AC-1174] Add event logs for bulk add collection access command * [AC-1174] Add User_BumpAccountRevisionDateByCollectionIds and database migration script * [AC-1174] Implement EF repository method * [AC-1174] Improve null checks * [AC-1174] Remove unnecessary BulkCollectionAccessRequestModel helpers * [AC-1174] Add unit tests for new controller endpoint * [AC-1174] Fix formatting * [AC-1174] Remove comment * [AC-1174] Remove redundant organizationId parameter * [AC-1174] Ensure user and group Ids are distinct * [AC-1174] Cleanup tests based on PR feedback * [AC-1174] Formatting * [AC-1174] Update CollectionGroup alias in the sproc * [AC-1174] Add some additional comments to SQL sproc * [AC-1174] Add comment explaining additional SaveChangesAsync call --------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com> * [AC-1646] Rename LimitCollectionCdOwnerAdmin column (#3300) * Rename LimitCollectionCdOwnerAdmin -> LimitCollectionCreationDeletion * Rename and bump migration script * [AC-1666] Removed EditAnyCollection from Create/Delete permission checks (#3301) * fix: remove EditAnyCollection from Create/Delete permission check, refs AC-1666 * fix: updated comment, refs AC-1666 * [AC-1669] Bug - Remove obsolete assignUserId from CollectionService.SaveAsync(...) (#3312) * fix: remove AssignUserId from CollectionService.SaveAsync, refs AC-1669 * fix: add manage access conditional before creating collection, refs AC-1669 * fix: move access logic for create/update, fix all tests, refs AC-1669 * fix: add CollectionAccessSelection fixture, update tests, update bad reqeuest message, refs AC-1669 * fix: format, refs AC-1669 * fix: update null params with specific arg.is null checks, refs Ac-1669 * fix: update attribute class name, refs AC-1669 * [AC-1713] [Flexible collections] Add feature flags to server (#3334) * Add feature flags for FlexibleCollections and BulkCollectionAccess * Flag new routes and behaviour --------- Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> * Add joint codeownership for auth handlers (#3346) * [AC-1717] Update default values for LimitCollectionCreationDeletion (#3365) * Change default value in organization create sproc to 1 * Drop old column name still present in some QA instances * Set LimitCollectionCreationDeletion value in code based on feature flag * Fix: add missing namespace after merging in master * Fix: add missing namespace after merging in master * [AC-1683] Fix DB migrations for new Manage permission (#3307) * [AC-1683] Update migration script and introduce V2 procedures and types * [AC-1683] Update repository calls to use new V2 procedures / types * [AC-1684] Update bulk add collection migration script to use new V2 type * [AC-1683] Undo Manage changes to more original procedures * [AC-1683] Restore whitespace changes * [AC-1683] Clarify comments regarding explicit column lists * [AC-1683] Update migration script dates * [AC-1683] Split the migration script for readability * [AC-1683] Re-name SelectReadOnlyArray_V2 to CollectionAccessSelectionType * [AC-1648] [Flexible Collections] Bump migration scripts before feature branch merge (#3371) * Bump dates on sql migration scripts * Bump date on ef migrations --------- Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com> Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com> Co-authored-by: Shane Melton <smelton@bitwarden.com> Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>
This commit is contained in:
@ -45,6 +45,8 @@ public static class FeatureFlagKeys
|
||||
public const string Fido2VaultCredentials = "fido2-vault-credentials";
|
||||
public const string AutofillV2 = "autofill-v2";
|
||||
public const string BrowserFilelessImport = "browser-fileless-import";
|
||||
public const string FlexibleCollections = "flexible-collections";
|
||||
public const string BulkCollectionAccess = "bulk-collection-access";
|
||||
public const string AutofillOverlay = "autofill-overlay";
|
||||
public const string ItemShare = "item-share";
|
||||
|
||||
|
@ -324,27 +324,16 @@ public class CurrentContext : ICurrentContext
|
||||
&& (o.Permissions?.AccessReports ?? false)) ?? false);
|
||||
}
|
||||
|
||||
public async Task<bool> CreateNewCollections(Guid orgId)
|
||||
{
|
||||
return await OrganizationManager(orgId) || (Organizations?.Any(o => o.Id == orgId
|
||||
&& (o.Permissions?.CreateNewCollections ?? false)) ?? false);
|
||||
}
|
||||
|
||||
public async Task<bool> EditAnyCollection(Guid orgId)
|
||||
{
|
||||
return await OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
|
||||
&& (o.Permissions?.EditAnyCollection ?? false)) ?? false);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAnyCollection(Guid orgId)
|
||||
{
|
||||
return await OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
|
||||
&& (o.Permissions?.DeleteAnyCollection ?? false)) ?? false);
|
||||
}
|
||||
|
||||
public async Task<bool> ViewAllCollections(Guid orgId)
|
||||
{
|
||||
return await EditAnyCollection(orgId) || await DeleteAnyCollection(orgId);
|
||||
var org = GetOrganization(orgId);
|
||||
return await EditAnyCollection(orgId) || (org != null && org.Permissions.DeleteAnyCollection);
|
||||
}
|
||||
|
||||
public async Task<bool> EditAssignedCollections(Guid orgId)
|
||||
@ -361,9 +350,20 @@ public class CurrentContext : ICurrentContext
|
||||
|
||||
public async Task<bool> ViewAssignedCollections(Guid orgId)
|
||||
{
|
||||
return await CreateNewCollections(orgId) // Required to display the existing collections under which the new collection can be nested
|
||||
|| await EditAssignedCollections(orgId)
|
||||
|| await DeleteAssignedCollections(orgId);
|
||||
/*
|
||||
* Required to display the existing collections under which the new collection can be nested.
|
||||
* Owner, Admin, Manager, and Provider checks are handled via the EditAssigned/DeleteAssigned context calls.
|
||||
* This entire method will be moved to the CollectionAuthorizationHandler in the future
|
||||
*/
|
||||
var canCreateNewCollections = false;
|
||||
var org = GetOrganization(orgId);
|
||||
if (org != null)
|
||||
{
|
||||
canCreateNewCollections = !org.LimitCollectionCreationDeletion || org.Permissions.CreateNewCollections;
|
||||
}
|
||||
return await EditAssignedCollections(orgId)
|
||||
|| await DeleteAssignedCollections(orgId)
|
||||
|| canCreateNewCollections;
|
||||
}
|
||||
|
||||
public async Task<bool> ManageGroups(Guid orgId)
|
||||
@ -512,6 +512,11 @@ public class CurrentContext : ICurrentContext
|
||||
return Providers;
|
||||
}
|
||||
|
||||
public CurrentContextOrganization GetOrganization(Guid orgId)
|
||||
{
|
||||
return Organizations?.Find(o => o.Id == orgId);
|
||||
}
|
||||
|
||||
private string GetClaimValue(Dictionary<string, IEnumerable<Claim>> claims, string type)
|
||||
{
|
||||
if (!claims.ContainsKey(type))
|
||||
|
@ -15,10 +15,12 @@ public class CurrentContextOrganization
|
||||
Type = orgUser.Type;
|
||||
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(orgUser.Permissions);
|
||||
AccessSecretsManager = orgUser.AccessSecretsManager && orgUser.UseSecretsManager && orgUser.Enabled;
|
||||
LimitCollectionCreationDeletion = orgUser.LimitCollectionCreationDeletion;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public OrganizationUserType Type { get; set; }
|
||||
public Permissions Permissions { get; set; }
|
||||
public Permissions Permissions { get; set; } = new();
|
||||
public bool AccessSecretsManager { get; set; }
|
||||
public bool LimitCollectionCreationDeletion { get; set; }
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
#nullable enable
|
||||
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Context;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
@ -41,9 +43,7 @@ public interface ICurrentContext
|
||||
Task<bool> AccessEventLogs(Guid orgId);
|
||||
Task<bool> AccessImportExport(Guid orgId);
|
||||
Task<bool> AccessReports(Guid orgId);
|
||||
Task<bool> CreateNewCollections(Guid orgId);
|
||||
Task<bool> EditAnyCollection(Guid orgId);
|
||||
Task<bool> DeleteAnyCollection(Guid orgId);
|
||||
Task<bool> ViewAllCollections(Guid orgId);
|
||||
Task<bool> EditAssignedCollections(Guid orgId);
|
||||
Task<bool> DeleteAssignedCollections(Guid orgId);
|
||||
@ -74,4 +74,5 @@ public interface ICurrentContext
|
||||
|
||||
Task<Guid?> ProviderIdForOrg(Guid orgId);
|
||||
bool AccessSecretsManager(Guid organizationId);
|
||||
CurrentContextOrganization? GetOrganization(Guid orgId);
|
||||
}
|
||||
|
@ -6,4 +6,5 @@ public class CollectionGroup
|
||||
public Guid GroupId { get; set; }
|
||||
public bool ReadOnly { get; set; }
|
||||
public bool HidePasswords { get; set; }
|
||||
public bool Manage { get; set; }
|
||||
}
|
||||
|
@ -6,4 +6,5 @@ public class CollectionUser
|
||||
public Guid OrganizationUserId { get; set; }
|
||||
public bool ReadOnly { get; set; }
|
||||
public bool HidePasswords { get; set; }
|
||||
public bool Manage { get; set; }
|
||||
}
|
||||
|
@ -78,6 +78,10 @@ public class Organization : ITableObject<Guid>, ISubscriber, IStorable, IStorabl
|
||||
public int? MaxAutoscaleSmSeats { get; set; }
|
||||
public int? MaxAutoscaleSmServiceAccounts { get; set; }
|
||||
public bool SecretsManagerBeta { get; set; }
|
||||
/// <summary>
|
||||
/// Refers to the ability for an organization to limit collection creation and deletion to owners and admins only
|
||||
/// </summary>
|
||||
public bool LimitCollectionCreationDeletion { get; set; }
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
|
@ -5,4 +5,5 @@ public class CollectionAccessSelection
|
||||
public Guid Id { get; set; }
|
||||
public bool ReadOnly { get; set; }
|
||||
public bool HidePasswords { get; set; }
|
||||
public bool Manage { get; set; }
|
||||
}
|
||||
|
@ -6,4 +6,5 @@ public class CollectionDetails : Collection
|
||||
{
|
||||
public bool ReadOnly { get; set; }
|
||||
public bool HidePasswords { get; set; }
|
||||
public bool Manage { get; set; }
|
||||
}
|
||||
|
@ -48,4 +48,5 @@ public class OrganizationUserOrganizationDetails
|
||||
public bool UsePasswordManager { get; set; }
|
||||
public int? SmSeats { get; set; }
|
||||
public int? SmServiceAccounts { get; set; }
|
||||
public bool LimitCollectionCreationDeletion { get; set; }
|
||||
}
|
||||
|
@ -142,6 +142,7 @@ public class SelfHostedOrganizationDetails : Organization
|
||||
RevisionDate = RevisionDate,
|
||||
MaxAutoscaleSeats = MaxAutoscaleSeats,
|
||||
OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,
|
||||
LimitCollectionCreationDeletion = LimitCollectionCreationDeletion,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,96 @@
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationCollections;
|
||||
|
||||
public class BulkAddCollectionAccessCommand : IBulkAddCollectionAccessCommand
|
||||
{
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IEventService _eventService;
|
||||
|
||||
public BulkAddCollectionAccessCommand(
|
||||
ICollectionRepository collectionRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IGroupRepository groupRepository,
|
||||
IEventService eventService)
|
||||
{
|
||||
_collectionRepository = collectionRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_groupRepository = groupRepository;
|
||||
_eventService = eventService;
|
||||
}
|
||||
|
||||
public async Task AddAccessAsync(ICollection<Collection> collections,
|
||||
ICollection<CollectionAccessSelection> users,
|
||||
ICollection<CollectionAccessSelection> groups)
|
||||
{
|
||||
await ValidateRequestAsync(collections, users, groups);
|
||||
|
||||
await _collectionRepository.CreateOrUpdateAccessForManyAsync(
|
||||
collections.First().OrganizationId,
|
||||
collections.Select(c => c.Id),
|
||||
users,
|
||||
groups
|
||||
);
|
||||
|
||||
await _eventService.LogCollectionEventsAsync(collections.Select(c =>
|
||||
(c, EventType.Collection_Updated, (DateTime?)DateTime.UtcNow)));
|
||||
}
|
||||
|
||||
private async Task ValidateRequestAsync(ICollection<Collection> collections, ICollection<CollectionAccessSelection> usersAccess, ICollection<CollectionAccessSelection> groupsAccess)
|
||||
{
|
||||
if (collections == null || collections.Count == 0)
|
||||
{
|
||||
throw new BadRequestException("No collections were provided.");
|
||||
}
|
||||
|
||||
var orgId = collections.First().OrganizationId;
|
||||
|
||||
if (collections.Any(c => c.OrganizationId != orgId))
|
||||
{
|
||||
throw new BadRequestException("All collections must belong to the same organization.");
|
||||
}
|
||||
|
||||
var collectionUserIds = usersAccess?.Select(u => u.Id).Distinct().ToList();
|
||||
|
||||
if (collectionUserIds is { Count: > 0 })
|
||||
{
|
||||
var users = await _organizationUserRepository.GetManyAsync(collectionUserIds);
|
||||
|
||||
if (users.Count != collectionUserIds.Count)
|
||||
{
|
||||
throw new BadRequestException("One or more users do not exist.");
|
||||
}
|
||||
|
||||
if (users.Any(u => u.OrganizationId != orgId))
|
||||
{
|
||||
throw new BadRequestException("One or more users do not belong to the same organization as the collection being assigned.");
|
||||
}
|
||||
}
|
||||
|
||||
var collectionGroupIds = groupsAccess?.Select(g => g.Id).Distinct().ToList();
|
||||
|
||||
if (collectionGroupIds is { Count: > 0 })
|
||||
{
|
||||
var groups = await _groupRepository.GetManyByManyIds(collectionGroupIds);
|
||||
|
||||
if (groups.Count != collectionGroupIds.Count)
|
||||
{
|
||||
throw new BadRequestException("One or more groups do not exist.");
|
||||
}
|
||||
|
||||
if (groups.Any(g => g.OrganizationId != orgId))
|
||||
{
|
||||
throw new BadRequestException("One or more groups do not belong to the same organization as the collection being assigned.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
|
||||
|
||||
public interface IBulkAddCollectionAccessCommand
|
||||
{
|
||||
Task AddAccessAsync(ICollection<Collection> collections,
|
||||
ICollection<CollectionAccessSelection> users, ICollection<CollectionAccessSelection> groups);
|
||||
}
|
@ -98,6 +98,7 @@ public static class OrganizationServiceCollectionExtensions
|
||||
public static void AddOrganizationCollectionCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IDeleteCollectionCommand, DeleteCollectionCommand>();
|
||||
services.AddScoped<IBulkAddCollectionAccessCommand, BulkAddCollectionAccessCommand>();
|
||||
}
|
||||
|
||||
private static void AddOrganizationGroupCommands(this IServiceCollection services)
|
||||
|
@ -20,4 +20,6 @@ public interface ICollectionRepository : IRepository<Collection, Guid>
|
||||
Task UpdateUsersAsync(Guid id, IEnumerable<CollectionAccessSelection> users);
|
||||
Task<ICollection<CollectionAccessSelection>> GetManyUsersByIdAsync(Guid id);
|
||||
Task DeleteManyAsync(IEnumerable<Guid> collectionIds);
|
||||
Task CreateOrUpdateAccessForManyAsync(Guid organizationId, IEnumerable<Guid> collectionIds,
|
||||
IEnumerable<CollectionAccessSelection> users, IEnumerable<CollectionAccessSelection> groups);
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ namespace Bit.Core.Services;
|
||||
|
||||
public interface ICollectionService
|
||||
{
|
||||
Task SaveAsync(Collection collection, IEnumerable<CollectionAccessSelection> groups = null, IEnumerable<CollectionAccessSelection> users = null, Guid? assignUserId = null);
|
||||
Task SaveAsync(Collection collection, IEnumerable<CollectionAccessSelection> groups = null, IEnumerable<CollectionAccessSelection> users = null);
|
||||
Task DeleteUserAsync(Collection collection, Guid organizationUserId);
|
||||
Task<IEnumerable<Collection>> GetOrganizationCollectionsAsync(Guid organizationId);
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ public class CollectionService : ICollectionService
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IReferenceEventService _referenceEventService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public CollectionService(
|
||||
IEventService eventService,
|
||||
@ -28,7 +29,8 @@ public class CollectionService : ICollectionService
|
||||
IUserRepository userRepository,
|
||||
IMailService mailService,
|
||||
IReferenceEventService referenceEventService,
|
||||
ICurrentContext currentContext)
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_eventService = eventService;
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -38,10 +40,11 @@ public class CollectionService : ICollectionService
|
||||
_mailService = mailService;
|
||||
_referenceEventService = referenceEventService;
|
||||
_currentContext = currentContext;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(Collection collection, IEnumerable<CollectionAccessSelection> groups = null,
|
||||
IEnumerable<CollectionAccessSelection> users = null, Guid? assignUserId = null)
|
||||
IEnumerable<CollectionAccessSelection> users = null)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(collection.OrganizationId);
|
||||
if (org == null)
|
||||
@ -49,6 +52,21 @@ public class CollectionService : ICollectionService
|
||||
throw new BadRequestException("Organization not found");
|
||||
}
|
||||
|
||||
var groupsList = groups?.ToList();
|
||||
var usersList = users?.ToList();
|
||||
|
||||
// If using Flexible Collections - a collection should always have someone with Can Manage permissions
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext))
|
||||
{
|
||||
var groupHasManageAccess = groupsList?.Any(g => g.Manage) ?? false;
|
||||
var userHasManageAccess = usersList?.Any(u => u.Manage) ?? false;
|
||||
if (!groupHasManageAccess && !userHasManageAccess)
|
||||
{
|
||||
throw new BadRequestException(
|
||||
"At least one member or group must have can manage permission.");
|
||||
}
|
||||
}
|
||||
|
||||
if (collection.Id == default(Guid))
|
||||
{
|
||||
if (org.MaxCollections.HasValue)
|
||||
@ -61,26 +79,13 @@ public class CollectionService : ICollectionService
|
||||
}
|
||||
}
|
||||
|
||||
await _collectionRepository.CreateAsync(collection, org.UseGroups ? groups : null, users);
|
||||
|
||||
// Assign a user to the newly created collection.
|
||||
if (assignUserId.HasValue)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, assignUserId.Value);
|
||||
if (orgUser != null && orgUser.Status == Enums.OrganizationUserStatusType.Confirmed)
|
||||
{
|
||||
await _collectionRepository.UpdateUsersAsync(collection.Id,
|
||||
new List<CollectionAccessSelection> {
|
||||
new CollectionAccessSelection { Id = orgUser.Id, ReadOnly = false } });
|
||||
}
|
||||
}
|
||||
|
||||
await _collectionRepository.CreateAsync(collection, org.UseGroups ? groupsList : null, usersList);
|
||||
await _eventService.LogCollectionEventAsync(collection, Enums.EventType.Collection_Created);
|
||||
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.CollectionCreated, org, _currentContext));
|
||||
}
|
||||
else
|
||||
{
|
||||
await _collectionRepository.ReplaceAsync(collection, org.UseGroups ? groups : null, users);
|
||||
await _collectionRepository.ReplaceAsync(collection, org.UseGroups ? groupsList : null, usersList);
|
||||
await _eventService.LogCollectionEventAsync(collection, Enums.EventType.Collection_Updated);
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ public class OrganizationService : IOrganizationService
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;
|
||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public OrganizationService(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -85,7 +86,8 @@ public class OrganizationService : IOrganizationService
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery,
|
||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand)
|
||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -114,6 +116,7 @@ public class OrganizationService : IOrganizationService
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;
|
||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
|
||||
@ -425,6 +428,9 @@ public class OrganizationService : IOrganizationService
|
||||
await ValidateSignUpPoliciesAsync(signup.Owner.Id);
|
||||
}
|
||||
|
||||
var flexibleCollectionsIsEnabled =
|
||||
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||
|
||||
var organization = new Organization
|
||||
{
|
||||
// Pre-generate the org id so that we can save it with the Stripe subscription..
|
||||
@ -462,6 +468,7 @@ public class OrganizationService : IOrganizationService
|
||||
Status = OrganizationStatusType.Created,
|
||||
UsePasswordManager = true,
|
||||
UseSecretsManager = signup.UseSecretsManager,
|
||||
LimitCollectionCreationDeletion = !flexibleCollectionsIsEnabled
|
||||
};
|
||||
|
||||
if (signup.UseSecretsManager)
|
||||
@ -2095,7 +2102,7 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
private async Task<bool> ValidateCustomPermissionsGrant(Guid organizationId, Permissions permissions)
|
||||
{
|
||||
if (permissions == null || await _currentContext.OrganizationOwner(organizationId) || await _currentContext.OrganizationAdmin(organizationId))
|
||||
if (permissions == null || await _currentContext.OrganizationAdmin(organizationId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@ -2140,16 +2147,6 @@ public class OrganizationService : IOrganizationService
|
||||
return false;
|
||||
}
|
||||
|
||||
if (permissions.CreateNewCollections && !await _currentContext.CreateNewCollections(organizationId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (permissions.DeleteAnyCollection && !await _currentContext.DeleteAnyCollection(organizationId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (permissions.DeleteAssignedCollections && !await _currentContext.DeleteAssignedCollections(organizationId))
|
||||
{
|
||||
return false;
|
||||
@ -2170,6 +2167,22 @@ public class OrganizationService : IOrganizationService
|
||||
return false;
|
||||
}
|
||||
|
||||
var org = _currentContext.GetOrganization(organizationId);
|
||||
if (org == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (permissions.CreateNewCollections && !org.Permissions.CreateNewCollections)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (permissions.DeleteAnyCollection && !org.Permissions.DeleteAnyCollection)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
40
src/Core/Utilities/BulkAuthorizationHandler.cs
Normal file
40
src/Core/Utilities/BulkAuthorizationHandler.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Core.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Allows a single authorization handler implementation to handle requirements for
|
||||
/// both singular or bulk operations on single or multiple resources.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRequirement">The type of the requirement to evaluate.</typeparam>
|
||||
/// <typeparam name="TResource">The type of the resource(s) that will be evaluated.</typeparam>
|
||||
public abstract class BulkAuthorizationHandler<TRequirement, TResource> : AuthorizationHandler<TRequirement>
|
||||
where TRequirement : IAuthorizationRequirement
|
||||
{
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement)
|
||||
{
|
||||
// Attempt to get the resource(s) from the context
|
||||
var bulkResources = GetBulkResourceFromContext(context);
|
||||
|
||||
// No resources of the expected type were found in the context, nothing to evaluate
|
||||
if (bulkResources == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await HandleRequirementAsync(context, requirement, bulkResources);
|
||||
}
|
||||
|
||||
private static ICollection<TResource> GetBulkResourceFromContext(AuthorizationHandlerContext context)
|
||||
{
|
||||
return context.Resource switch
|
||||
{
|
||||
TResource resource => new List<TResource> { resource },
|
||||
IEnumerable<TResource> resources => resources.ToList(),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement,
|
||||
ICollection<TResource> resources);
|
||||
}
|
Reference in New Issue
Block a user