mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -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:
@ -767,4 +767,23 @@ public class OrganizationsController : Controller
|
||||
|
||||
return new OrganizationSsoResponseModel(organization, _globalSettings, ssoConfig);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/collection-management")]
|
||||
[RequireFeature(FeatureFlagKeys.FlexibleCollections)]
|
||||
public async Task<OrganizationResponseModel> PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!await _currentContext.OrganizationOwner(id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _organizationService.UpdateAsync(model.ToOrganization(organization));
|
||||
return new OrganizationResponseModel(organization);
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ public class OrganizationResponseModel : ResponseModel
|
||||
SmServiceAccounts = organization.SmServiceAccounts;
|
||||
MaxAutoscaleSmSeats = organization.MaxAutoscaleSmSeats;
|
||||
MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts;
|
||||
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
@ -93,6 +94,7 @@ public class OrganizationResponseModel : ResponseModel
|
||||
public int? SmServiceAccounts { get; set; }
|
||||
public int? MaxAutoscaleSmSeats { get; set; }
|
||||
public int? MaxAutoscaleSmServiceAccounts { get; set; }
|
||||
public bool LimitCollectionCreationDeletion { get; set; }
|
||||
}
|
||||
|
||||
public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
||||
|
@ -60,6 +60,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete;
|
||||
FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil;
|
||||
AccessSecretsManager = organization.AccessSecretsManager;
|
||||
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
||||
|
||||
if (organization.SsoConfig != null)
|
||||
{
|
||||
@ -113,4 +114,5 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
public DateTime? FamilySponsorshipValidUntil { get; set; }
|
||||
public bool? FamilySponsorshipToDelete { get; set; }
|
||||
public bool AccessSecretsManager { get; set; }
|
||||
public bool LimitCollectionCreationDeletion { get; set; }
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -19,22 +22,33 @@ public class CollectionsController : Controller
|
||||
private readonly ICollectionService _collectionService;
|
||||
private readonly IDeleteCollectionCommand _deleteCollectionCommand;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IBulkAddCollectionAccessCommand _bulkAddCollectionAccessCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public CollectionsController(
|
||||
ICollectionRepository collectionRepository,
|
||||
ICollectionService collectionService,
|
||||
IDeleteCollectionCommand deleteCollectionCommand,
|
||||
IUserService userService,
|
||||
ICurrentContext currentContext)
|
||||
IAuthorizationService authorizationService,
|
||||
ICurrentContext currentContext,
|
||||
IBulkAddCollectionAccessCommand bulkAddCollectionAccessCommand,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_collectionRepository = collectionRepository;
|
||||
_collectionService = collectionService;
|
||||
_deleteCollectionCommand = deleteCollectionCommand;
|
||||
_userService = userService;
|
||||
_authorizationService = authorizationService;
|
||||
_currentContext = currentContext;
|
||||
_bulkAddCollectionAccessCommand = bulkAddCollectionAccessCommand;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<CollectionResponseModel> Get(Guid orgId, Guid id)
|
||||
{
|
||||
@ -62,6 +76,7 @@ public class CollectionsController : Controller
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new CollectionAccessDetailsResponseModel(collection, access.Groups, access.Users);
|
||||
}
|
||||
else
|
||||
@ -72,6 +87,7 @@ public class CollectionsController : Controller
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new CollectionAccessDetailsResponseModel(collection, access.Groups, access.Users);
|
||||
}
|
||||
}
|
||||
@ -79,13 +95,15 @@ public class CollectionsController : Controller
|
||||
[HttpGet("details")]
|
||||
public async Task<ListResponseModel<CollectionAccessDetailsResponseModel>> GetManyWithDetails(Guid orgId)
|
||||
{
|
||||
if (!await ViewAtLeastOneCollectionAsync(orgId) && !await _currentContext.ManageUsers(orgId) && !await _currentContext.ManageGroups(orgId))
|
||||
if (!await ViewAtLeastOneCollectionAsync(orgId) && !await _currentContext.ManageUsers(orgId) &&
|
||||
!await _currentContext.ManageGroups(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// We always need to know which collections the current user is assigned to
|
||||
var assignedOrgCollections = await _collectionRepository.GetManyByUserIdWithAccessAsync(_currentContext.UserId.Value, orgId);
|
||||
var assignedOrgCollections =
|
||||
await _collectionRepository.GetManyByUserIdWithAccessAsync(_currentContext.UserId.Value, orgId);
|
||||
|
||||
if (await _currentContext.ViewAllCollections(orgId) || await _currentContext.ManageUsers(orgId))
|
||||
{
|
||||
@ -141,8 +159,10 @@ public class CollectionsController : Controller
|
||||
{
|
||||
var collection = model.ToCollection(orgId);
|
||||
|
||||
if (!await CanCreateCollection(orgId, collection.Id) &&
|
||||
!await CanEditCollectionAsync(orgId, collection.Id))
|
||||
var authorized = FlexibleCollectionsIsEnabled
|
||||
? (await _authorizationService.AuthorizeAsync(User, collection, CollectionOperations.Create)).Succeeded
|
||||
: await CanCreateCollection(orgId, collection.Id) || await CanEditCollectionAsync(orgId, collection.Id);
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -150,10 +170,7 @@ public class CollectionsController : Controller
|
||||
var groups = model.Groups?.Select(g => g.ToSelectionReadOnly());
|
||||
var users = model.Users?.Select(g => g.ToSelectionReadOnly());
|
||||
|
||||
var assignUserToCollection = !(await _currentContext.EditAnyCollection(orgId)) &&
|
||||
await _currentContext.EditAssignedCollections(orgId);
|
||||
|
||||
await _collectionService.SaveAsync(collection, groups, users, assignUserToCollection ? _currentContext.UserId : null);
|
||||
await _collectionService.SaveAsync(collection, groups, users);
|
||||
return new CollectionResponseModel(collection);
|
||||
}
|
||||
|
||||
@ -185,32 +202,77 @@ public class CollectionsController : Controller
|
||||
await _collectionRepository.UpdateUsersAsync(collection.Id, model?.Select(g => g.ToSelectionReadOnly()));
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(Guid orgId, Guid id)
|
||||
[HttpPost("bulk-access")]
|
||||
[RequireFeature(FeatureFlagKeys.BulkCollectionAccess)]
|
||||
// Also gated behind Flexible Collections flag because it only has new authorization logic.
|
||||
// Could be removed if legacy authorization logic were implemented for many collections.
|
||||
[RequireFeature(FeatureFlagKeys.FlexibleCollections)]
|
||||
public async Task PostBulkCollectionAccess([FromBody] BulkCollectionAccessRequestModel model)
|
||||
{
|
||||
if (!await CanDeleteCollectionAsync(orgId, id))
|
||||
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.CollectionIds);
|
||||
|
||||
if (collections.Count != model.CollectionIds.Count())
|
||||
{
|
||||
throw new NotFoundException("One or more collections not found.");
|
||||
}
|
||||
|
||||
var result = await _authorizationService.AuthorizeAsync(User, collections, CollectionOperations.ModifyAccess);
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _bulkAddCollectionAccessCommand.AddAccessAsync(
|
||||
collections,
|
||||
model.Users?.Select(u => u.ToSelectionReadOnly()).ToList(),
|
||||
model.Groups?.Select(g => g.ToSelectionReadOnly()).ToList());
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(Guid orgId, Guid id)
|
||||
{
|
||||
var collection = await GetCollectionAsync(id, orgId);
|
||||
|
||||
var authorized = FlexibleCollectionsIsEnabled
|
||||
? (await _authorizationService.AuthorizeAsync(User, collection, CollectionOperations.Delete)).Succeeded
|
||||
: await CanDeleteCollectionAsync(orgId, id);
|
||||
if (!authorized)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _deleteCollectionCommand.DeleteAsync(collection);
|
||||
}
|
||||
|
||||
[HttpDelete("")]
|
||||
[HttpPost("delete")]
|
||||
public async Task DeleteMany([FromBody] CollectionBulkDeleteRequestModel model)
|
||||
public async Task DeleteMany(Guid orgId, [FromBody] CollectionBulkDeleteRequestModel model)
|
||||
{
|
||||
var orgId = new Guid(model.OrganizationId);
|
||||
var collectionIds = model.Ids.Select(i => new Guid(i));
|
||||
if (!await _currentContext.DeleteAssignedCollections(orgId) && !await _currentContext.DeleteAnyCollection(orgId))
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
// New flexible collections logic
|
||||
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Ids);
|
||||
var result = await _authorizationService.AuthorizeAsync(User, collections, CollectionOperations.Delete);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _deleteCollectionCommand.DeleteManyAsync(collections);
|
||||
return;
|
||||
}
|
||||
|
||||
// Old pre-flexible collections logic follows
|
||||
if (!await _currentContext.DeleteAssignedCollections(orgId) && !await DeleteAnyCollection(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var userCollections = await _collectionService.GetOrganizationCollectionsAsync(orgId);
|
||||
var filteredCollections = userCollections.Where(c => collectionIds.Contains(c.Id) && c.OrganizationId == orgId);
|
||||
var filteredCollections = userCollections
|
||||
.Where(c => model.Ids.Contains(c.Id) && c.OrganizationId == orgId);
|
||||
|
||||
if (!filteredCollections.Any())
|
||||
{
|
||||
@ -248,15 +310,26 @@ public class CollectionsController : Controller
|
||||
return collection;
|
||||
}
|
||||
|
||||
private void DeprecatedPermissionsGuard()
|
||||
{
|
||||
if (FlexibleCollectionsIsEnabled)
|
||||
{
|
||||
throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF.");
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
|
||||
private async Task<bool> CanCreateCollection(Guid orgId, Guid collectionId)
|
||||
{
|
||||
DeprecatedPermissionsGuard();
|
||||
|
||||
if (collectionId != default)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _currentContext.CreateNewCollections(orgId);
|
||||
return await _currentContext.OrganizationManager(orgId) || (_currentContext.Organizations?.Any(o => o.Id == orgId &&
|
||||
(o.Permissions?.CreateNewCollections ?? false)) ?? false);
|
||||
}
|
||||
|
||||
private async Task<bool> CanEditCollectionAsync(Guid orgId, Guid collectionId)
|
||||
@ -273,34 +346,49 @@ public class CollectionsController : Controller
|
||||
|
||||
if (await _currentContext.EditAssignedCollections(orgId))
|
||||
{
|
||||
var collectionDetails = await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value);
|
||||
var collectionDetails =
|
||||
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value);
|
||||
return collectionDetails != null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
|
||||
private async Task<bool> CanDeleteCollectionAsync(Guid orgId, Guid collectionId)
|
||||
{
|
||||
DeprecatedPermissionsGuard();
|
||||
|
||||
if (collectionId == default)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await _currentContext.DeleteAnyCollection(orgId))
|
||||
if (await DeleteAnyCollection(orgId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (await _currentContext.DeleteAssignedCollections(orgId))
|
||||
{
|
||||
var collectionDetails = await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value);
|
||||
var collectionDetails =
|
||||
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value);
|
||||
return collectionDetails != null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[Obsolete("Pre-Flexible Collections logic. Will be replaced by CollectionsAuthorizationHandler.")]
|
||||
private async Task<bool> DeleteAnyCollection(Guid orgId)
|
||||
{
|
||||
DeprecatedPermissionsGuard();
|
||||
|
||||
return await _currentContext.OrganizationAdmin(orgId) ||
|
||||
(_currentContext.Organizations?.Any(o => o.Id == orgId
|
||||
&& (o.Permissions?.DeleteAnyCollection ?? false)) ?? false);
|
||||
}
|
||||
|
||||
private async Task<bool> CanViewCollectionAsync(Guid orgId, Guid collectionId)
|
||||
{
|
||||
if (collectionId == default)
|
||||
@ -315,7 +403,8 @@ public class CollectionsController : Controller
|
||||
|
||||
if (await _currentContext.ViewAssignedCollections(orgId))
|
||||
{
|
||||
var collectionDetails = await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value);
|
||||
var collectionDetails =
|
||||
await _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value);
|
||||
return collectionDetails != null;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
namespace Bit.Api.Models.Request;
|
||||
|
||||
public class BulkCollectionAccessRequestModel
|
||||
{
|
||||
public IEnumerable<Guid> CollectionIds { get; set; }
|
||||
public IEnumerable<SelectionReadOnlyRequestModel> Groups { get; set; }
|
||||
public IEnumerable<SelectionReadOnlyRequestModel> Users { get; set; }
|
||||
}
|
@ -34,8 +34,7 @@ public class CollectionRequestModel
|
||||
public class CollectionBulkDeleteRequestModel
|
||||
{
|
||||
[Required]
|
||||
public IEnumerable<string> Ids { get; set; }
|
||||
public string OrganizationId { get; set; }
|
||||
public IEnumerable<Guid> Ids { get; set; }
|
||||
}
|
||||
|
||||
public class CollectionWithIdRequestModel : CollectionRequestModel
|
||||
|
@ -0,0 +1,14 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Api.Models.Request.Organizations;
|
||||
|
||||
public class OrganizationCollectionManagementUpdateRequestModel
|
||||
{
|
||||
public bool LimitCreateDeleteOwnerAdmin { get; set; }
|
||||
|
||||
public virtual Organization ToOrganization(Organization existingOrganization)
|
||||
{
|
||||
existingOrganization.LimitCollectionCreationDeletion = LimitCreateDeleteOwnerAdmin;
|
||||
return existingOrganization;
|
||||
}
|
||||
}
|
@ -6,17 +6,19 @@ namespace Bit.Api.Models.Request;
|
||||
public class SelectionReadOnlyRequestModel
|
||||
{
|
||||
[Required]
|
||||
public string Id { get; set; }
|
||||
public Guid Id { get; set; }
|
||||
public bool ReadOnly { get; set; }
|
||||
public bool HidePasswords { get; set; }
|
||||
public bool Manage { get; set; }
|
||||
|
||||
public CollectionAccessSelection ToSelectionReadOnly()
|
||||
{
|
||||
return new CollectionAccessSelection
|
||||
{
|
||||
Id = new Guid(Id),
|
||||
Id = Id,
|
||||
ReadOnly = ReadOnly,
|
||||
HidePasswords = HidePasswords,
|
||||
Manage = Manage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -33,10 +33,12 @@ public class CollectionDetailsResponseModel : CollectionResponseModel
|
||||
{
|
||||
ReadOnly = collectionDetails.ReadOnly;
|
||||
HidePasswords = collectionDetails.HidePasswords;
|
||||
Manage = collectionDetails.Manage;
|
||||
}
|
||||
|
||||
public bool ReadOnly { get; set; }
|
||||
public bool HidePasswords { get; set; }
|
||||
public bool Manage { get; set; }
|
||||
}
|
||||
|
||||
public class CollectionAccessDetailsResponseModel : CollectionResponseModel
|
||||
|
@ -14,9 +14,11 @@ public class SelectionReadOnlyResponseModel
|
||||
Id = selection.Id;
|
||||
ReadOnly = selection.ReadOnly;
|
||||
HidePasswords = selection.HidePasswords;
|
||||
Manage = selection.Manage;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public bool ReadOnly { get; set; }
|
||||
public bool HidePasswords { get; set; }
|
||||
public bool Manage { get; set; }
|
||||
}
|
||||
|
@ -137,6 +137,9 @@ public class Startup
|
||||
services.AddOrganizationSubscriptionServices();
|
||||
services.AddCoreLocalizationServices();
|
||||
|
||||
// Authorization Handlers
|
||||
services.AddAuthorizationHandlers();
|
||||
|
||||
//health check
|
||||
if (!globalSettings.SelfHosted)
|
||||
{
|
||||
|
@ -1,7 +1,9 @@
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.SharedWeb.Health;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
namespace Bit.Api.Utilities;
|
||||
@ -115,4 +117,9 @@ public static class ServiceCollectionExtensions
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void AddAuthorizationHandlers(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IAuthorizationHandler, CollectionAuthorizationHandler>();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,177 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Handles authorization logic for Collection objects, including access permissions for users and groups.
|
||||
/// This uses new logic implemented in the Flexible Collections initiative.
|
||||
/// </summary>
|
||||
public class CollectionAuthorizationHandler : BulkAuthorizationHandler<CollectionOperationRequirement, Collection>
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public CollectionAuthorizationHandler(ICurrentContext currentContext, ICollectionRepository collectionRepository,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_collectionRepository = collectionRepository;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||
CollectionOperationRequirement requirement, ICollection<Collection> resources)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext))
|
||||
{
|
||||
// Flexible collections is OFF, should not be using this handler
|
||||
throw new FeatureUnavailableException("Flexible collections is OFF when it should be ON.");
|
||||
}
|
||||
|
||||
// Establish pattern of authorization handler null checking passed resources
|
||||
if (resources == null || !resources.Any())
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_currentContext.UserId.HasValue)
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
var targetOrganizationId = resources.First().OrganizationId;
|
||||
|
||||
// Ensure all target collections belong to the same organization
|
||||
if (resources.Any(tc => tc.OrganizationId != targetOrganizationId))
|
||||
{
|
||||
throw new BadRequestException("Requested collections must belong to the same organization.");
|
||||
}
|
||||
|
||||
// Acting user is not a member of the target organization, fail
|
||||
var org = _currentContext.GetOrganization(targetOrganizationId);
|
||||
if (org == null)
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (requirement)
|
||||
{
|
||||
case not null when requirement == CollectionOperations.Create:
|
||||
await CanCreateAsync(context, requirement, org);
|
||||
break;
|
||||
|
||||
case not null when requirement == CollectionOperations.Delete:
|
||||
await CanDeleteAsync(context, requirement, resources, org);
|
||||
break;
|
||||
|
||||
case not null when requirement == CollectionOperations.ModifyAccess:
|
||||
await CanManageCollectionAccessAsync(context, requirement, resources, org);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CanCreateAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement,
|
||||
CurrentContextOrganization org)
|
||||
{
|
||||
// If false, all organization members are allowed to create collections
|
||||
if (!org.LimitCollectionCreationDeletion)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
// Owners, Admins, Providers, and users with CreateNewCollections permission can always create collections
|
||||
if (
|
||||
org.Type is OrganizationUserType.Owner or OrganizationUserType.Admin ||
|
||||
org.Permissions is { CreateNewCollections: true } ||
|
||||
await _currentContext.ProviderUserForOrgAsync(org.Id))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
context.Fail();
|
||||
}
|
||||
|
||||
private async Task CanDeleteAsync(AuthorizationHandlerContext context, CollectionOperationRequirement requirement,
|
||||
ICollection<Collection> resources, CurrentContextOrganization org)
|
||||
{
|
||||
// Owners, Admins, Providers, and users with DeleteAnyCollection permission can always delete collections
|
||||
if (
|
||||
org.Type is OrganizationUserType.Owner or OrganizationUserType.Admin ||
|
||||
org.Permissions is { DeleteAnyCollection: true } ||
|
||||
await _currentContext.ProviderUserForOrgAsync(org.Id))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
// The limit collection management setting is enabled and we are not an Admin (above condition), fail
|
||||
if (org.LimitCollectionCreationDeletion)
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
// Other members types should have the Manage capability for all collections being deleted
|
||||
var manageableCollectionIds =
|
||||
(await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId!.Value))
|
||||
.Where(c => c.Manage && c.OrganizationId == org.Id)
|
||||
.Select(c => c.Id)
|
||||
.ToHashSet();
|
||||
|
||||
// The acting user does not have permission to manage all target collections, fail
|
||||
if (resources.Any(c => !manageableCollectionIds.Contains(c.Id)))
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the acting user is allowed to manage access permissions for the target collections.
|
||||
/// </summary>
|
||||
private async Task CanManageCollectionAccessAsync(AuthorizationHandlerContext context,
|
||||
IAuthorizationRequirement requirement, ICollection<Collection> targetCollections, CurrentContextOrganization org)
|
||||
{
|
||||
// Owners, Admins, Providers, and users with EditAnyCollection permission can always manage collection access
|
||||
if (
|
||||
org.Permissions is { EditAnyCollection: true } ||
|
||||
org.Type is OrganizationUserType.Owner or OrganizationUserType.Admin ||
|
||||
await _currentContext.ProviderUserForOrgAsync(org.Id))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return;
|
||||
}
|
||||
|
||||
// List of collection Ids the acting user is allowed to manage
|
||||
var manageableCollectionIds =
|
||||
(await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId!.Value))
|
||||
.Where(c => c.Manage && c.OrganizationId == org.Id)
|
||||
.Select(c => c.Id)
|
||||
.ToHashSet();
|
||||
|
||||
// The acting user does not have permission to manage all target collections, fail
|
||||
if (targetCollections.Any(tc => !manageableCollectionIds.Contains(tc.Id)))
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
|
||||
namespace Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
|
||||
public class CollectionOperationRequirement : OperationAuthorizationRequirement { }
|
||||
|
||||
public static class CollectionOperations
|
||||
{
|
||||
public static readonly CollectionOperationRequirement Create = new() { Name = nameof(Create) };
|
||||
public static readonly CollectionOperationRequirement Delete = new() { Name = nameof(Delete) };
|
||||
/// <summary>
|
||||
/// The operation that represents creating, updating, or removing collection access.
|
||||
/// Combined together to allow for a single requirement to be used for each operation
|
||||
/// as they all currently share the same underlying authorization logic.
|
||||
/// </summary>
|
||||
public static readonly CollectionOperationRequirement ModifyAccess = new() { Name = nameof(ModifyAccess) };
|
||||
}
|
@ -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);
|
||||
}
|
@ -73,7 +73,8 @@ public class GroupRepository : Repository<Group, Guid>, IGroupRepository
|
||||
{
|
||||
Id = c.CollectionId,
|
||||
HidePasswords = c.HidePasswords,
|
||||
ReadOnly = c.ReadOnly
|
||||
ReadOnly = c.ReadOnly,
|
||||
Manage = c.Manage
|
||||
}
|
||||
).ToList() ?? new List<CollectionAccessSelection>())
|
||||
).ToList();
|
||||
@ -141,7 +142,7 @@ public class GroupRepository : Repository<Group, Guid>, IGroupRepository
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.ExecuteAsync(
|
||||
$"[{Schema}].[Group_CreateWithCollections]",
|
||||
$"[{Schema}].[Group_CreateWithCollections_V2]",
|
||||
objWithCollections,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
@ -155,7 +156,7 @@ public class GroupRepository : Repository<Group, Guid>, IGroupRepository
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.ExecuteAsync(
|
||||
$"[{Schema}].[Group_UpdateWithCollections]",
|
||||
$"[{Schema}].[Group_UpdateWithCollections_V2]",
|
||||
objWithCollections,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ public static class DapperHelpers
|
||||
public static DataTable ToArrayTVP(this IEnumerable<CollectionAccessSelection> values)
|
||||
{
|
||||
var table = new DataTable();
|
||||
table.SetTypeName("[dbo].[SelectionReadOnlyArray]");
|
||||
table.SetTypeName("[dbo].[CollectionAccessSelectionType]");
|
||||
|
||||
var idColumn = new DataColumn("Id", typeof(Guid));
|
||||
table.Columns.Add(idColumn);
|
||||
@ -40,6 +40,8 @@ public static class DapperHelpers
|
||||
table.Columns.Add(readOnlyColumn);
|
||||
var hidePasswordsColumn = new DataColumn("HidePasswords", typeof(bool));
|
||||
table.Columns.Add(hidePasswordsColumn);
|
||||
var manageColumn = new DataColumn("Manage", typeof(bool));
|
||||
table.Columns.Add(manageColumn);
|
||||
|
||||
if (values != null)
|
||||
{
|
||||
@ -49,6 +51,7 @@ public static class DapperHelpers
|
||||
row[idColumn] = value.Id;
|
||||
row[readOnlyColumn] = value.ReadOnly;
|
||||
row[hidePasswordsColumn] = value.HidePasswords;
|
||||
row[manageColumn] = value.Manage;
|
||||
table.Rows.Add(row);
|
||||
}
|
||||
}
|
||||
|
@ -121,7 +121,8 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
||||
{
|
||||
Id = g.GroupId,
|
||||
HidePasswords = g.HidePasswords,
|
||||
ReadOnly = g.ReadOnly
|
||||
ReadOnly = g.ReadOnly,
|
||||
Manage = g.Manage
|
||||
}).ToList() ?? new List<CollectionAccessSelection>(),
|
||||
Users = users
|
||||
.FirstOrDefault(u => u.Key == collection.Id)?
|
||||
@ -129,7 +130,8 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
||||
{
|
||||
Id = c.OrganizationUserId,
|
||||
HidePasswords = c.HidePasswords,
|
||||
ReadOnly = c.ReadOnly
|
||||
ReadOnly = c.ReadOnly,
|
||||
Manage = c.Manage
|
||||
}).ToList() ?? new List<CollectionAccessSelection>()
|
||||
}
|
||||
)
|
||||
@ -163,7 +165,8 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
||||
{
|
||||
Id = g.GroupId,
|
||||
HidePasswords = g.HidePasswords,
|
||||
ReadOnly = g.ReadOnly
|
||||
ReadOnly = g.ReadOnly,
|
||||
Manage = g.Manage
|
||||
}).ToList() ?? new List<CollectionAccessSelection>(),
|
||||
Users = users
|
||||
.FirstOrDefault(u => u.Key == collection.Id)?
|
||||
@ -171,7 +174,8 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
||||
{
|
||||
Id = c.OrganizationUserId,
|
||||
HidePasswords = c.HidePasswords,
|
||||
ReadOnly = c.ReadOnly
|
||||
ReadOnly = c.ReadOnly,
|
||||
Manage = c.Manage
|
||||
}).ToList() ?? new List<CollectionAccessSelection>()
|
||||
}
|
||||
)
|
||||
@ -217,7 +221,7 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.ExecuteAsync(
|
||||
$"[{Schema}].[Collection_CreateWithGroupsAndUsers]",
|
||||
$"[{Schema}].[Collection_CreateWithGroupsAndUsers_V2]",
|
||||
objWithGroupsAndUsers,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
@ -233,7 +237,7 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.ExecuteAsync(
|
||||
$"[{Schema}].[Collection_UpdateWithGroupsAndUsers]",
|
||||
$"[{Schema}].[Collection_UpdateWithGroupsAndUsers_V2]",
|
||||
objWithGroupsAndUsers,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
@ -248,6 +252,21 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateOrUpdateAccessForManyAsync(Guid organizationId, IEnumerable<Guid> collectionIds,
|
||||
IEnumerable<CollectionAccessSelection> users, IEnumerable<CollectionAccessSelection> groups)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var usersArray = users != null ? users.ToArrayTVP() : Enumerable.Empty<CollectionAccessSelection>().ToArrayTVP();
|
||||
var groupsArray = groups != null ? groups.ToArrayTVP() : Enumerable.Empty<CollectionAccessSelection>().ToArrayTVP();
|
||||
|
||||
var results = await connection.ExecuteAsync(
|
||||
$"[{Schema}].[Collection_CreateOrUpdateAccessForMany]",
|
||||
new { OrganizationId = organizationId, CollectionIds = collectionIds.ToGuidIdArrayTVP(), Users = usersArray, Groups = groupsArray },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateUserAsync(Guid collectionId, Guid organizationUserId)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
@ -275,7 +294,7 @@ public class CollectionRepository : Repository<Collection, Guid>, ICollectionRep
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.ExecuteAsync(
|
||||
$"[{Schema}].[CollectionUser_UpdateUsers]",
|
||||
$"[{Schema}].[CollectionUser_UpdateUsers_V2]",
|
||||
new { CollectionId = id, Users = users.ToArrayTVP() },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
@ -267,7 +267,8 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
{
|
||||
Id = uc.CollectionId,
|
||||
ReadOnly = uc.ReadOnly,
|
||||
HidePasswords = uc.HidePasswords
|
||||
HidePasswords = uc.HidePasswords,
|
||||
Manage = uc.Manage
|
||||
}).ToList() ?? new List<CollectionAccessSelection>();
|
||||
}
|
||||
}
|
||||
@ -325,7 +326,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.ExecuteAsync(
|
||||
$"[{Schema}].[OrganizationUser_CreateWithCollections]",
|
||||
$"[{Schema}].[OrganizationUser_CreateWithCollections_V2]",
|
||||
objWithCollections,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
@ -342,7 +343,7 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.ExecuteAsync(
|
||||
$"[{Schema}].[OrganizationUser_UpdateWithCollections]",
|
||||
$"[{Schema}].[OrganizationUser_UpdateWithCollections_V2]",
|
||||
objWithCollections,
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ public class GroupRepository : Repository<AdminConsoleEntities.Group, Group, Gui
|
||||
GroupId = grp.Id,
|
||||
ReadOnly = y.ReadOnly,
|
||||
HidePasswords = y.HidePasswords,
|
||||
Manage = y.Manage,
|
||||
});
|
||||
await dbContext.CollectionGroups.AddRangeAsync(collectionGroups);
|
||||
await dbContext.SaveChangesAsync();
|
||||
@ -68,6 +69,7 @@ public class GroupRepository : Repository<AdminConsoleEntities.Group, Group, Gui
|
||||
Id = c.CollectionId,
|
||||
ReadOnly = c.ReadOnly,
|
||||
HidePasswords = c.HidePasswords,
|
||||
Manage = c.Manage,
|
||||
}).ToList();
|
||||
return new Tuple<AdminConsoleEntities.Group, ICollection<CollectionAccessSelection>>(
|
||||
grp, collections);
|
||||
@ -110,7 +112,8 @@ public class GroupRepository : Repository<AdminConsoleEntities.Group, Group, Gui
|
||||
{
|
||||
Id = c.CollectionId,
|
||||
HidePasswords = c.HidePasswords,
|
||||
ReadOnly = c.ReadOnly
|
||||
ReadOnly = c.ReadOnly,
|
||||
Manage = c.Manage
|
||||
}
|
||||
).ToList() ?? new List<CollectionAccessSelection>())
|
||||
).ToList();
|
||||
@ -204,12 +207,14 @@ public class GroupRepository : Repository<AdminConsoleEntities.Group, Group, Gui
|
||||
GroupId = group.Id,
|
||||
ReadOnly = requestedCollection.ReadOnly,
|
||||
HidePasswords = requestedCollection.HidePasswords,
|
||||
Manage = requestedCollection.Manage
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
existingCollectionGroup.ReadOnly = requestedCollection.ReadOnly;
|
||||
existingCollectionGroup.HidePasswords = requestedCollection.HidePasswords;
|
||||
existingCollectionGroup.Manage = requestedCollection.Manage;
|
||||
}
|
||||
|
||||
var requestedCollectionIds = requestedCollections.Select(c => c.Id);
|
||||
|
@ -68,6 +68,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
GroupId = g.Id,
|
||||
ReadOnly = g.ReadOnly,
|
||||
HidePasswords = g.HidePasswords,
|
||||
Manage = g.Manage
|
||||
});
|
||||
await dbContext.AddRangeAsync(collectionGroups);
|
||||
}
|
||||
@ -85,6 +86,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
OrganizationUserId = u.Id,
|
||||
ReadOnly = u.ReadOnly,
|
||||
HidePasswords = u.HidePasswords,
|
||||
Manage = u.Manage
|
||||
});
|
||||
await dbContext.AddRangeAsync(collectionUsers);
|
||||
}
|
||||
@ -130,6 +132,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
Id = cg.GroupId,
|
||||
ReadOnly = cg.ReadOnly,
|
||||
HidePasswords = cg.HidePasswords,
|
||||
Manage = cg.Manage
|
||||
};
|
||||
var groups = await groupQuery.ToArrayAsync();
|
||||
|
||||
@ -140,6 +143,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
Id = cg.OrganizationUserId,
|
||||
ReadOnly = cg.ReadOnly,
|
||||
HidePasswords = cg.HidePasswords,
|
||||
Manage = cg.Manage
|
||||
};
|
||||
var users = await userQuery.ToArrayAsync();
|
||||
var access = new CollectionAccessDetails { Users = users, Groups = groups };
|
||||
@ -161,6 +165,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
Id = cg.GroupId,
|
||||
ReadOnly = cg.ReadOnly,
|
||||
HidePasswords = cg.HidePasswords,
|
||||
Manage = cg.Manage
|
||||
};
|
||||
var groups = await groupQuery.ToArrayAsync();
|
||||
|
||||
@ -171,6 +176,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
Id = cg.OrganizationUserId,
|
||||
ReadOnly = cg.ReadOnly,
|
||||
HidePasswords = cg.HidePasswords,
|
||||
Manage = cg.Manage,
|
||||
};
|
||||
var users = await userQuery.ToArrayAsync();
|
||||
var access = new CollectionAccessDetails { Users = users, Groups = groups };
|
||||
@ -207,7 +213,8 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
{
|
||||
Id = g.GroupId,
|
||||
HidePasswords = g.HidePasswords,
|
||||
ReadOnly = g.ReadOnly
|
||||
ReadOnly = g.ReadOnly,
|
||||
Manage = g.Manage
|
||||
}).ToList() ?? new List<CollectionAccessSelection>(),
|
||||
Users = users
|
||||
.FirstOrDefault(u => u.Key == collection.Id)?
|
||||
@ -215,7 +222,8 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
{
|
||||
Id = c.OrganizationUserId,
|
||||
HidePasswords = c.HidePasswords,
|
||||
ReadOnly = c.ReadOnly
|
||||
ReadOnly = c.ReadOnly,
|
||||
Manage = c.Manage
|
||||
}).ToList() ?? new List<CollectionAccessSelection>()
|
||||
}
|
||||
)
|
||||
@ -251,7 +259,8 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
{
|
||||
Id = g.GroupId,
|
||||
HidePasswords = g.HidePasswords,
|
||||
ReadOnly = g.ReadOnly
|
||||
ReadOnly = g.ReadOnly,
|
||||
Manage = g.Manage
|
||||
}).ToList() ?? new List<CollectionAccessSelection>(),
|
||||
Users = users
|
||||
.FirstOrDefault(u => u.Key == collection.Id)?
|
||||
@ -259,7 +268,8 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
{
|
||||
Id = c.OrganizationUserId,
|
||||
HidePasswords = c.HidePasswords,
|
||||
ReadOnly = c.ReadOnly
|
||||
ReadOnly = c.ReadOnly,
|
||||
Manage = c.Manage
|
||||
}).ToList() ?? new List<CollectionAccessSelection>()
|
||||
}
|
||||
)
|
||||
@ -329,6 +339,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
ExternalId = collectionGroup.Key.ExternalId,
|
||||
ReadOnly = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.ReadOnly))),
|
||||
HidePasswords = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
||||
Manage = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.Manage))),
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
@ -353,6 +364,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
ExternalId = collectionGroup.Key.ExternalId,
|
||||
ReadOnly = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.ReadOnly))),
|
||||
HidePasswords = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.HidePasswords))),
|
||||
Manage = Convert.ToBoolean(collectionGroup.Min(c => Convert.ToInt32(c.Manage))),
|
||||
}).ToListAsync();
|
||||
}
|
||||
}
|
||||
@ -371,6 +383,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
Id = cu.OrganizationUserId,
|
||||
ReadOnly = cu.ReadOnly,
|
||||
HidePasswords = cu.HidePasswords,
|
||||
Manage = cu.Manage
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
@ -415,6 +428,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
OrganizationUserId = requestedUser.Id,
|
||||
HidePasswords = requestedUser.HidePasswords,
|
||||
ReadOnly = requestedUser.ReadOnly,
|
||||
Manage = requestedUser.Manage
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@ -422,6 +436,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
// It already exists, update it
|
||||
existingCollectionUser.HidePasswords = requestedUser.HidePasswords;
|
||||
existingCollectionUser.ReadOnly = requestedUser.ReadOnly;
|
||||
existingCollectionUser.Manage = requestedUser.Manage;
|
||||
dbContext.CollectionUsers.Update(existingCollectionUser);
|
||||
}
|
||||
|
||||
@ -458,6 +473,97 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateOrUpdateAccessForManyAsync(Guid organizationId, IEnumerable<Guid> collectionIds,
|
||||
IEnumerable<CollectionAccessSelection> users, IEnumerable<CollectionAccessSelection> groups)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var collectionIdsList = collectionIds.ToList();
|
||||
|
||||
if (users != null)
|
||||
{
|
||||
var existingCollectionUsers = await dbContext.CollectionUsers
|
||||
.Where(cu => collectionIdsList.Contains(cu.CollectionId))
|
||||
.ToDictionaryAsync(x => (x.CollectionId, x.OrganizationUserId));
|
||||
|
||||
var requestedUsers = users.ToList();
|
||||
|
||||
foreach (var collectionId in collectionIdsList)
|
||||
{
|
||||
foreach (var requestedUser in requestedUsers)
|
||||
{
|
||||
if (!existingCollectionUsers.TryGetValue(
|
||||
(collectionId, requestedUser.Id),
|
||||
out var existingCollectionUser)
|
||||
)
|
||||
{
|
||||
// This is a brand new entry
|
||||
dbContext.CollectionUsers.Add(new CollectionUser
|
||||
{
|
||||
CollectionId = collectionId,
|
||||
OrganizationUserId = requestedUser.Id,
|
||||
HidePasswords = requestedUser.HidePasswords,
|
||||
ReadOnly = requestedUser.ReadOnly,
|
||||
Manage = requestedUser.Manage
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// It already exists, update it
|
||||
existingCollectionUser.HidePasswords = requestedUser.HidePasswords;
|
||||
existingCollectionUser.ReadOnly = requestedUser.ReadOnly;
|
||||
existingCollectionUser.Manage = requestedUser.Manage;
|
||||
dbContext.CollectionUsers.Update(existingCollectionUser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (groups != null)
|
||||
{
|
||||
var existingCollectionGroups = await dbContext.CollectionGroups
|
||||
.Where(cu => collectionIdsList.Contains(cu.CollectionId))
|
||||
.ToDictionaryAsync(x => (x.CollectionId, x.GroupId));
|
||||
|
||||
var requestedGroups = groups.ToList();
|
||||
|
||||
foreach (var collectionId in collectionIdsList)
|
||||
{
|
||||
foreach (var requestedGroup in requestedGroups)
|
||||
{
|
||||
if (!existingCollectionGroups.TryGetValue(
|
||||
(collectionId, requestedGroup.Id),
|
||||
out var existingCollectionGroup)
|
||||
)
|
||||
{
|
||||
// This is a brand new entry
|
||||
dbContext.CollectionGroups.Add(new CollectionGroup()
|
||||
{
|
||||
CollectionId = collectionId,
|
||||
GroupId = requestedGroup.Id,
|
||||
HidePasswords = requestedGroup.HidePasswords,
|
||||
ReadOnly = requestedGroup.ReadOnly,
|
||||
Manage = requestedGroup.Manage
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// It already exists, update it
|
||||
existingCollectionGroup.HidePasswords = requestedGroup.HidePasswords;
|
||||
existingCollectionGroup.ReadOnly = requestedGroup.ReadOnly;
|
||||
existingCollectionGroup.Manage = requestedGroup.Manage;
|
||||
dbContext.CollectionGroups.Update(existingCollectionGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Need to save the new collection users/groups before running the bump revision code
|
||||
await dbContext.SaveChangesAsync();
|
||||
await dbContext.UserBumpAccountRevisionDateByCollectionIdsAsync(collectionIdsList, organizationId);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReplaceCollectionGroupsAsync(DatabaseContext dbContext, Core.Entities.Collection collection, IEnumerable<CollectionAccessSelection> groups)
|
||||
{
|
||||
var groupsInOrg = dbContext.Groups.Where(g => g.OrganizationId == collection.OrganizationId);
|
||||
@ -487,13 +593,15 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
GroupId = x.g.Id,
|
||||
ReadOnly = groups.FirstOrDefault(g => g.Id == x.g.Id).ReadOnly,
|
||||
HidePasswords = groups.FirstOrDefault(g => g.Id == x.g.Id).HidePasswords,
|
||||
Manage = groups.FirstOrDefault(g => g.Id == x.g.Id).Manage
|
||||
}).ToList();
|
||||
var update = union
|
||||
.Where(
|
||||
x => x.g != null &&
|
||||
x.cg != null &&
|
||||
(x.cg.ReadOnly != groups.FirstOrDefault(g => g.Id == x.g.Id).ReadOnly ||
|
||||
x.cg.HidePasswords != groups.FirstOrDefault(g => g.Id == x.g.Id).HidePasswords)
|
||||
x.cg.HidePasswords != groups.FirstOrDefault(g => g.Id == x.g.Id).HidePasswords ||
|
||||
x.cg.Manage != groups.FirstOrDefault(g => g.Id == x.g.Id).Manage)
|
||||
)
|
||||
.Select(x => new CollectionGroup
|
||||
{
|
||||
@ -501,6 +609,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
GroupId = x.g.Id,
|
||||
ReadOnly = groups.FirstOrDefault(g => g.Id == x.g.Id).ReadOnly,
|
||||
HidePasswords = groups.FirstOrDefault(g => g.Id == x.g.Id).HidePasswords,
|
||||
Manage = groups.FirstOrDefault(g => g.Id == x.g.Id).Manage,
|
||||
});
|
||||
var delete = union
|
||||
.Where(
|
||||
@ -549,13 +658,15 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
OrganizationUserId = x.u.Id,
|
||||
ReadOnly = users.FirstOrDefault(u => u.Id == x.u.Id).ReadOnly,
|
||||
HidePasswords = users.FirstOrDefault(u => u.Id == x.u.Id).HidePasswords,
|
||||
Manage = users.FirstOrDefault(u => u.Id == x.u.Id).Manage,
|
||||
}).ToList();
|
||||
var update = union
|
||||
.Where(
|
||||
x => x.u != null &&
|
||||
x.cu != null &&
|
||||
(x.cu.ReadOnly != users.FirstOrDefault(u => u.Id == x.u.Id).ReadOnly ||
|
||||
x.cu.HidePasswords != users.FirstOrDefault(u => u.Id == x.u.Id).HidePasswords)
|
||||
x.cu.HidePasswords != users.FirstOrDefault(u => u.Id == x.u.Id).HidePasswords ||
|
||||
x.cu.Manage != users.FirstOrDefault(u => u.Id == x.u.Id).Manage)
|
||||
)
|
||||
.Select(x => new CollectionUser
|
||||
{
|
||||
@ -563,6 +674,7 @@ public class CollectionRepository : Repository<Core.Entities.Collection, Collect
|
||||
OrganizationUserId = x.u.Id,
|
||||
ReadOnly = users.FirstOrDefault(u => u.Id == x.u.Id).ReadOnly,
|
||||
HidePasswords = users.FirstOrDefault(u => u.Id == x.u.Id).HidePasswords,
|
||||
Manage = users.FirstOrDefault(u => u.Id == x.u.Id).Manage,
|
||||
});
|
||||
var delete = union
|
||||
.Where(
|
||||
|
@ -110,6 +110,9 @@ public class DatabaseContext : DbContext
|
||||
eGroup.Property(c => c.Id).ValueGeneratedNever();
|
||||
eInstallation.Property(c => c.Id).ValueGeneratedNever();
|
||||
eOrganization.Property(c => c.Id).ValueGeneratedNever();
|
||||
eOrganization.Property(c => c.LimitCollectionCreationDeletion)
|
||||
.ValueGeneratedNever()
|
||||
.HasDefaultValue(true);
|
||||
eOrganizationSponsorship.Property(c => c.Id).ValueGeneratedNever();
|
||||
eOrganizationUser.Property(c => c.Id).ValueGeneratedNever();
|
||||
ePolicy.Property(c => c.Id).ValueGeneratedNever();
|
||||
|
@ -74,6 +74,39 @@ public static class DatabaseContextExtensions
|
||||
UpdateUserRevisionDate(users);
|
||||
}
|
||||
|
||||
public static async Task UserBumpAccountRevisionDateByCollectionIdsAsync(this DatabaseContext context, IEnumerable<Guid> collectionIds, Guid organizationId)
|
||||
{
|
||||
var query = from u in context.Users
|
||||
from c in context.Collections
|
||||
join ou in context.OrganizationUsers
|
||||
on u.Id equals ou.UserId
|
||||
join cu in context.CollectionUsers
|
||||
on new { ou.AccessAll, OrganizationUserId = ou.Id, CollectionId = c.Id } equals
|
||||
new { AccessAll = false, cu.OrganizationUserId, cu.CollectionId } into cu_g
|
||||
from cu in cu_g.DefaultIfEmpty()
|
||||
join gu in context.GroupUsers
|
||||
on new { CollectionId = (Guid?)cu.CollectionId, ou.AccessAll, OrganizationUserId = ou.Id } equals
|
||||
new { CollectionId = (Guid?)null, AccessAll = false, gu.OrganizationUserId } into gu_g
|
||||
from gu in gu_g.DefaultIfEmpty()
|
||||
join g in context.Groups
|
||||
on gu.GroupId equals g.Id into g_g
|
||||
from g in g_g.DefaultIfEmpty()
|
||||
join cg in context.CollectionGroups
|
||||
on new { g.AccessAll, gu.GroupId, CollectionId = c.Id } equals
|
||||
new { AccessAll = false, cg.GroupId, cg.CollectionId } into cg_g
|
||||
from cg in cg_g.DefaultIfEmpty()
|
||||
where ou.OrganizationId == organizationId && collectionIds.Contains(c.Id) &&
|
||||
ou.Status == OrganizationUserStatusType.Confirmed &&
|
||||
(cu.CollectionId != null ||
|
||||
cg.CollectionId != null ||
|
||||
ou.AccessAll == true ||
|
||||
g.AccessAll == true)
|
||||
select u;
|
||||
|
||||
var users = await query.ToListAsync();
|
||||
UpdateUserRevisionDate(users);
|
||||
}
|
||||
|
||||
public static async Task UserBumpAccountRevisionDateByOrganizationUserIdAsync(this DatabaseContext context, Guid organizationUserId)
|
||||
{
|
||||
var query = from u in context.Users
|
||||
|
@ -33,6 +33,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
OrganizationUserId = organizationUser.Id,
|
||||
ReadOnly = y.ReadOnly,
|
||||
HidePasswords = y.HidePasswords,
|
||||
Manage = y.Manage
|
||||
});
|
||||
await dbContext.CollectionUsers.AddRangeAsync(collectionUsers);
|
||||
await dbContext.SaveChangesAsync();
|
||||
@ -146,6 +147,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
Id = cu.CollectionId,
|
||||
ReadOnly = cu.ReadOnly,
|
||||
HidePasswords = cu.HidePasswords,
|
||||
Manage = cu.Manage,
|
||||
});
|
||||
return new Tuple<Core.Entities.OrganizationUser, ICollection<CollectionAccessSelection>>(
|
||||
organizationUser, collections.ToList());
|
||||
@ -240,6 +242,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
Id = cu.CollectionId,
|
||||
ReadOnly = cu.ReadOnly,
|
||||
HidePasswords = cu.HidePasswords,
|
||||
Manage = cu.Manage
|
||||
}).ToListAsync();
|
||||
return new Tuple<OrganizationUserUserDetails, ICollection<CollectionAccessSelection>>(organizationUserUserDetails, collections);
|
||||
}
|
||||
@ -365,7 +368,8 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
{
|
||||
Id = cu.CollectionId,
|
||||
ReadOnly = cu.ReadOnly,
|
||||
HidePasswords = cu.HidePasswords
|
||||
HidePasswords = cu.HidePasswords,
|
||||
Manage = cu.Manage,
|
||||
}).ToList() ?? new List<CollectionAccessSelection>();
|
||||
}
|
||||
}
|
||||
@ -445,6 +449,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
OrganizationUserId = obj.Id,
|
||||
HidePasswords = requestedCollection.HidePasswords,
|
||||
ReadOnly = requestedCollection.ReadOnly,
|
||||
Manage = requestedCollection.Manage
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@ -452,6 +457,7 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
// It already exists, update it
|
||||
existingCollectionUser.HidePasswords = requestedCollection.HidePasswords;
|
||||
existingCollectionUser.ReadOnly = requestedCollection.ReadOnly;
|
||||
existingCollectionUser.Manage = requestedCollection.Manage;
|
||||
dbContext.CollectionUsers.Update(existingCollectionUser);
|
||||
}
|
||||
|
||||
|
@ -58,6 +58,8 @@ public class UserCollectionDetailsQuery : IQuery<CollectionDetails>
|
||||
!((bool?)x.cu.ReadOnly ?? (bool?)x.cg.ReadOnly ?? false) ? false : true,
|
||||
HidePasswords = x.ou.AccessAll || x.g.AccessAll ||
|
||||
!((bool?)x.cu.HidePasswords ?? (bool?)x.cg.HidePasswords ?? false) ? false : true,
|
||||
Manage = x.ou.AccessAll || x.g.AccessAll ||
|
||||
!((bool?)x.cu.Manage ?? (bool?)x.cg.Manage ?? false) ? false : true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,15 @@ SELECT
|
||||
OR COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) = 0
|
||||
THEN 0
|
||||
ELSE 1
|
||||
END [HidePasswords]
|
||||
END [HidePasswords],
|
||||
CASE
|
||||
WHEN
|
||||
OU.[AccessAll] = 1
|
||||
OR G.[AccessAll] = 1
|
||||
OR COALESCE(CU.[Manage], CG.[Manage], 0) = 0
|
||||
THEN 0
|
||||
ELSE 1
|
||||
END [Manage]
|
||||
FROM
|
||||
[dbo].[CollectionView] C
|
||||
INNER JOIN
|
||||
|
@ -7,9 +7,10 @@ BEGIN
|
||||
SELECT
|
||||
[GroupId] [Id],
|
||||
[ReadOnly],
|
||||
[HidePasswords]
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
FROM
|
||||
[dbo].[CollectionGroup]
|
||||
WHERE
|
||||
[CollectionId] = @CollectionId
|
||||
END
|
||||
END
|
||||
|
@ -7,9 +7,10 @@ BEGIN
|
||||
SELECT
|
||||
[OrganizationUserId] [Id],
|
||||
[ReadOnly],
|
||||
[HidePasswords]
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
FROM
|
||||
[dbo].[CollectionUser]
|
||||
WHERE
|
||||
[CollectionId] = @CollectionId
|
||||
END
|
||||
END
|
||||
|
@ -31,9 +31,14 @@ BEGIN
|
||||
OR [Target].[HidePasswords] != [Source].[HidePasswords]
|
||||
)
|
||||
|
||||
-- Insert
|
||||
INSERT INTO
|
||||
[dbo].[CollectionUser]
|
||||
-- Insert (with column list because a value for Manage is not being provided)
|
||||
INSERT INTO [dbo].[CollectionUser]
|
||||
(
|
||||
[CollectionId],
|
||||
[OrganizationUserId],
|
||||
[ReadOnly],
|
||||
[HidePasswords]
|
||||
)
|
||||
SELECT
|
||||
@CollectionId,
|
||||
[Source].[Id],
|
||||
@ -53,7 +58,7 @@ BEGIN
|
||||
[CollectionId] = @CollectionId
|
||||
AND [OrganizationUserId] = [Source].[Id]
|
||||
)
|
||||
|
||||
|
||||
-- Delete
|
||||
DELETE
|
||||
CU
|
||||
@ -71,4 +76,4 @@ BEGIN
|
||||
)
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @CollectionId, @OrgId
|
||||
END
|
||||
END
|
||||
|
@ -0,0 +1,83 @@
|
||||
CREATE PROCEDURE [dbo].[CollectionUser_UpdateUsers_V2]
|
||||
@CollectionId UNIQUEIDENTIFIER,
|
||||
@Users AS [dbo].[CollectionAccessSelectionType] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
DECLARE @OrgId UNIQUEIDENTIFIER = (
|
||||
SELECT TOP 1
|
||||
[OrganizationId]
|
||||
FROM
|
||||
[dbo].[Collection]
|
||||
WHERE
|
||||
[Id] = @CollectionId
|
||||
)
|
||||
|
||||
-- Update
|
||||
UPDATE
|
||||
[Target]
|
||||
SET
|
||||
[Target].[ReadOnly] = [Source].[ReadOnly],
|
||||
[Target].[HidePasswords] = [Source].[HidePasswords],
|
||||
[Target].[Manage] = [Source].[Manage]
|
||||
FROM
|
||||
[dbo].[CollectionUser] [Target]
|
||||
INNER JOIN
|
||||
@Users [Source] ON [Source].[Id] = [Target].[OrganizationUserId]
|
||||
WHERE
|
||||
[Target].[CollectionId] = @CollectionId
|
||||
AND (
|
||||
[Target].[ReadOnly] != [Source].[ReadOnly]
|
||||
OR [Target].[HidePasswords] != [Source].[HidePasswords]
|
||||
OR [Target].[Manage] != [Source].[Manage]
|
||||
)
|
||||
|
||||
-- Insert
|
||||
INSERT INTO [dbo].[CollectionUser]
|
||||
(
|
||||
[CollectionId],
|
||||
[OrganizationUserId],
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
)
|
||||
SELECT
|
||||
@CollectionId,
|
||||
[Source].[Id],
|
||||
[Source].[ReadOnly],
|
||||
[Source].[HidePasswords],
|
||||
[Source].[Manage]
|
||||
FROM
|
||||
@Users [Source]
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] OU ON [Source].[Id] = OU.[Id] AND OU.[OrganizationId] = @OrgId
|
||||
WHERE
|
||||
NOT EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
[dbo].[CollectionUser]
|
||||
WHERE
|
||||
[CollectionId] = @CollectionId
|
||||
AND [OrganizationUserId] = [Source].[Id]
|
||||
)
|
||||
|
||||
-- Delete
|
||||
DELETE
|
||||
CU
|
||||
FROM
|
||||
[dbo].[CollectionUser] CU
|
||||
WHERE
|
||||
CU.[CollectionId] = @CollectionId
|
||||
AND NOT EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
@Users
|
||||
WHERE
|
||||
[Id] = CU.[OrganizationUserId]
|
||||
)
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @CollectionId, @OrgId
|
||||
END
|
@ -0,0 +1,113 @@
|
||||
CREATE PROCEDURE [dbo].[Collection_CreateOrUpdateAccessForMany]
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@CollectionIds AS [dbo].[GuidIdArray] READONLY,
|
||||
@Groups AS [dbo].[CollectionAccessSelectionType] READONLY,
|
||||
@Users AS [dbo].[CollectionAccessSelectionType] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
-- Groups
|
||||
;WITH [NewCollectionGroups] AS (
|
||||
SELECT
|
||||
cId.[Id] AS [CollectionId],
|
||||
cg.[Id] AS [GroupId],
|
||||
cg.[ReadOnly],
|
||||
cg.[HidePasswords],
|
||||
cg.[Manage]
|
||||
FROM
|
||||
@Groups AS cg
|
||||
CROSS JOIN -- Create a CollectionGroup record for every CollectionId
|
||||
@CollectionIds cId
|
||||
INNER JOIN
|
||||
[dbo].[Group] g ON cg.[Id] = g.[Id]
|
||||
WHERE
|
||||
g.[OrganizationId] = @OrganizationId
|
||||
)
|
||||
MERGE
|
||||
[dbo].[CollectionGroup] as [Target]
|
||||
USING
|
||||
[NewCollectionGroups] AS [Source]
|
||||
ON
|
||||
[Target].[CollectionId] = [Source].[CollectionId]
|
||||
AND [Target].[GroupId] = [Source].[GroupId]
|
||||
-- Update the target if any values are different from the source
|
||||
WHEN MATCHED AND EXISTS(
|
||||
SELECT [Source].[ReadOnly], [Source].[HidePasswords], [Source].[Manage]
|
||||
EXCEPT
|
||||
SELECT [Target].[ReadOnly], [Target].[HidePasswords], [Target].[Manage]
|
||||
) THEN UPDATE SET
|
||||
[Target].[ReadOnly] = [Source].[ReadOnly],
|
||||
[Target].[HidePasswords] = [Source].[HidePasswords],
|
||||
[Target].[Manage] = [Source].[Manage]
|
||||
WHEN NOT MATCHED BY TARGET
|
||||
THEN INSERT
|
||||
(
|
||||
[CollectionId],
|
||||
[GroupId],
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
[Source].[CollectionId],
|
||||
[Source].[GroupId],
|
||||
[Source].[ReadOnly],
|
||||
[Source].[HidePasswords],
|
||||
[Source].[Manage]
|
||||
);
|
||||
|
||||
-- Users
|
||||
;WITH [NewCollectionUsers] AS (
|
||||
SELECT
|
||||
cId.[Id] AS [CollectionId],
|
||||
cu.[Id] AS [OrganizationUserId],
|
||||
cu.[ReadOnly],
|
||||
cu.[HidePasswords],
|
||||
cu.[Manage]
|
||||
FROM
|
||||
@Users AS cu
|
||||
CROSS JOIN -- Create a CollectionUser record for every CollectionId
|
||||
@CollectionIds cId
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] u ON cu.[Id] = u.[Id]
|
||||
WHERE
|
||||
u.[OrganizationId] = @OrganizationId
|
||||
)
|
||||
MERGE
|
||||
[dbo].[CollectionUser] as [Target]
|
||||
USING
|
||||
[NewCollectionUsers] AS [Source]
|
||||
ON
|
||||
[Target].[CollectionId] = [Source].[CollectionId]
|
||||
AND [Target].[OrganizationUserId] = [Source].[OrganizationUserId]
|
||||
-- Update the target if any values are different from the source
|
||||
WHEN MATCHED AND EXISTS(
|
||||
SELECT [Source].[ReadOnly], [Source].[HidePasswords], [Source].[Manage]
|
||||
EXCEPT
|
||||
SELECT [Target].[ReadOnly], [Target].[HidePasswords], [Target].[Manage]
|
||||
) THEN UPDATE SET
|
||||
[Target].[ReadOnly] = [Source].[ReadOnly],
|
||||
[Target].[HidePasswords] = [Source].[HidePasswords],
|
||||
[Target].[Manage] = [Source].[Manage]
|
||||
WHEN NOT MATCHED BY TARGET
|
||||
THEN INSERT
|
||||
(
|
||||
[CollectionId],
|
||||
[OrganizationUserId],
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
[Source].[CollectionId],
|
||||
[Source].[OrganizationUserId],
|
||||
[Source].[ReadOnly],
|
||||
[Source].[HidePasswords],
|
||||
[Source].[Manage]
|
||||
);
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionIds] @CollectionIds, @OrganizationId
|
||||
END
|
@ -0,0 +1,73 @@
|
||||
CREATE PROCEDURE [dbo].[Collection_CreateWithGroupsAndUsers_V2]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Name VARCHAR(MAX),
|
||||
@ExternalId NVARCHAR(300),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@Groups AS [dbo].[CollectionAccessSelectionType] READONLY,
|
||||
@Users AS [dbo].[CollectionAccessSelectionType] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
EXEC [dbo].[Collection_Create] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate
|
||||
|
||||
-- Groups
|
||||
;WITH [AvailableGroupsCTE] AS(
|
||||
SELECT
|
||||
[Id]
|
||||
FROM
|
||||
[dbo].[Group]
|
||||
WHERE
|
||||
[OrganizationId] = @OrganizationId
|
||||
)
|
||||
INSERT INTO [dbo].[CollectionGroup]
|
||||
(
|
||||
[CollectionId],
|
||||
[GroupId],
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
)
|
||||
SELECT
|
||||
@Id,
|
||||
[Id],
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
FROM
|
||||
@Groups
|
||||
WHERE
|
||||
[Id] IN (SELECT [Id] FROM [AvailableGroupsCTE])
|
||||
|
||||
-- Users
|
||||
;WITH [AvailableUsersCTE] AS(
|
||||
SELECT
|
||||
[Id]
|
||||
FROM
|
||||
[dbo].[OrganizationUser]
|
||||
WHERE
|
||||
[OrganizationId] = @OrganizationId
|
||||
)
|
||||
INSERT INTO [dbo].[CollectionUser]
|
||||
(
|
||||
[CollectionId],
|
||||
[OrganizationUserId],
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
)
|
||||
SELECT
|
||||
@Id,
|
||||
[Id],
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
FROM
|
||||
@Users
|
||||
WHERE
|
||||
[Id] IN (SELECT [Id] FROM [AvailableUsersCTE])
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId
|
||||
END
|
@ -12,7 +12,8 @@ BEGIN
|
||||
RevisionDate,
|
||||
ExternalId,
|
||||
MIN([ReadOnly]) AS [ReadOnly],
|
||||
MIN([HidePasswords]) AS [HidePasswords]
|
||||
MIN([HidePasswords]) AS [HidePasswords],
|
||||
MIN([Manage]) AS [Manage]
|
||||
FROM
|
||||
[dbo].[UserCollectionDetails](@UserId)
|
||||
WHERE
|
||||
|
@ -12,7 +12,8 @@ BEGIN
|
||||
RevisionDate,
|
||||
ExternalId,
|
||||
MIN([ReadOnly]) AS [ReadOnly],
|
||||
MIN([HidePasswords]) AS [HidePasswords]
|
||||
MIN([HidePasswords]) AS [HidePasswords],
|
||||
MIN([Manage]) AS [Manage]
|
||||
FROM
|
||||
[dbo].[UserCollectionDetails](@UserId)
|
||||
GROUP BY
|
||||
|
@ -12,7 +12,8 @@ BEGIN
|
||||
RevisionDate DATETIME2(7),
|
||||
ExternalId NVARCHAR(300),
|
||||
ReadOnly BIT,
|
||||
HidePasswords BIT)
|
||||
HidePasswords BIT,
|
||||
Manage BIT)
|
||||
|
||||
INSERT INTO @TempUserCollections EXEC [dbo].[Collection_ReadByUserId] @UserId
|
||||
|
||||
@ -35,4 +36,4 @@ BEGIN
|
||||
INNER JOIN
|
||||
@TempUserCollections C ON C.[Id] = CU.[CollectionId]
|
||||
|
||||
END
|
||||
END
|
||||
|
@ -24,14 +24,21 @@ BEGIN
|
||||
)
|
||||
MERGE
|
||||
[dbo].[CollectionGroup] AS [Target]
|
||||
USING
|
||||
USING
|
||||
@Groups AS [Source]
|
||||
ON
|
||||
[Target].[CollectionId] = @Id
|
||||
AND [Target].[GroupId] = [Source].[Id]
|
||||
WHEN NOT MATCHED BY TARGET
|
||||
AND [Source].[Id] IN (SELECT [Id] FROM [AvailableGroupsCTE]) THEN
|
||||
INSERT VALUES
|
||||
INSERT -- With column list because a value for Manage is not being provided
|
||||
(
|
||||
[CollectionId],
|
||||
[GroupId],
|
||||
[ReadOnly],
|
||||
[HidePasswords]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
[Source].[Id],
|
||||
@ -60,14 +67,21 @@ BEGIN
|
||||
)
|
||||
MERGE
|
||||
[dbo].[CollectionUser] AS [Target]
|
||||
USING
|
||||
USING
|
||||
@Users AS [Source]
|
||||
ON
|
||||
[Target].[CollectionId] = @Id
|
||||
AND [Target].[OrganizationUserId] = [Source].[Id]
|
||||
WHEN NOT MATCHED BY TARGET
|
||||
AND [Source].[Id] IN (SELECT [Id] FROM [AvailableGroupsCTE]) THEN
|
||||
INSERT VALUES
|
||||
INSERT -- With column list because a value for Manage is not being provided
|
||||
(
|
||||
[CollectionId],
|
||||
[OrganizationUserId],
|
||||
[ReadOnly],
|
||||
[HidePasswords]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
[Source].[Id],
|
||||
@ -86,4 +100,4 @@ BEGIN
|
||||
;
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId
|
||||
END
|
||||
END
|
||||
|
@ -0,0 +1,111 @@
|
||||
CREATE PROCEDURE [dbo].[Collection_UpdateWithGroupsAndUsers_V2]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Name VARCHAR(MAX),
|
||||
@ExternalId NVARCHAR(300),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@Groups AS [dbo].[CollectionAccessSelectionType] READONLY,
|
||||
@Users AS [dbo].[CollectionAccessSelectionType] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate
|
||||
|
||||
-- Groups
|
||||
;WITH [AvailableGroupsCTE] AS(
|
||||
SELECT
|
||||
Id
|
||||
FROM
|
||||
[dbo].[Group]
|
||||
WHERE
|
||||
OrganizationId = @OrganizationId
|
||||
)
|
||||
MERGE
|
||||
[dbo].[CollectionGroup] AS [Target]
|
||||
USING
|
||||
@Groups AS [Source]
|
||||
ON
|
||||
[Target].[CollectionId] = @Id
|
||||
AND [Target].[GroupId] = [Source].[Id]
|
||||
WHEN NOT MATCHED BY TARGET
|
||||
AND [Source].[Id] IN (SELECT [Id] FROM [AvailableGroupsCTE]) THEN
|
||||
INSERT -- Add explicit column list
|
||||
(
|
||||
[CollectionId],
|
||||
[GroupId],
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
[Source].[Id],
|
||||
[Source].[ReadOnly],
|
||||
[Source].[HidePasswords],
|
||||
[Source].[Manage]
|
||||
)
|
||||
WHEN MATCHED AND (
|
||||
[Target].[ReadOnly] != [Source].[ReadOnly]
|
||||
OR [Target].[HidePasswords] != [Source].[HidePasswords]
|
||||
OR [Target].[Manage] != [Source].[Manage]
|
||||
) THEN
|
||||
UPDATE SET [Target].[ReadOnly] = [Source].[ReadOnly],
|
||||
[Target].[HidePasswords] = [Source].[HidePasswords],
|
||||
[Target].[Manage] = [Source].[Manage]
|
||||
WHEN NOT MATCHED BY SOURCE
|
||||
AND [Target].[CollectionId] = @Id THEN
|
||||
DELETE
|
||||
;
|
||||
|
||||
-- Users
|
||||
;WITH [AvailableGroupsCTE] AS(
|
||||
SELECT
|
||||
Id
|
||||
FROM
|
||||
[dbo].[OrganizationUser]
|
||||
WHERE
|
||||
OrganizationId = @OrganizationId
|
||||
)
|
||||
MERGE
|
||||
[dbo].[CollectionUser] AS [Target]
|
||||
USING
|
||||
@Users AS [Source]
|
||||
ON
|
||||
[Target].[CollectionId] = @Id
|
||||
AND [Target].[OrganizationUserId] = [Source].[Id]
|
||||
WHEN NOT MATCHED BY TARGET
|
||||
AND [Source].[Id] IN (SELECT [Id] FROM [AvailableGroupsCTE]) THEN
|
||||
INSERT
|
||||
(
|
||||
[CollectionId],
|
||||
[OrganizationUserId],
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
[Source].[Id],
|
||||
[Source].[ReadOnly],
|
||||
[Source].[HidePasswords],
|
||||
[Source].[Manage]
|
||||
)
|
||||
WHEN MATCHED AND (
|
||||
[Target].[ReadOnly] != [Source].[ReadOnly]
|
||||
OR [Target].[HidePasswords] != [Source].[HidePasswords]
|
||||
OR [Target].[Manage] != [Source].[Manage]
|
||||
) THEN
|
||||
UPDATE SET [Target].[ReadOnly] = [Source].[ReadOnly],
|
||||
[Target].[HidePasswords] = [Source].[HidePasswords],
|
||||
[Target].[Manage] = [Source].[Manage]
|
||||
WHEN NOT MATCHED BY SOURCE
|
||||
AND [Target].[CollectionId] = @Id THEN
|
||||
DELETE
|
||||
;
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId
|
||||
END
|
@ -0,0 +1,44 @@
|
||||
CREATE PROCEDURE [dbo].[Group_CreateWithCollections_V2]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Name NVARCHAR(100),
|
||||
@AccessAll BIT,
|
||||
@ExternalId NVARCHAR(300),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@Collections AS [dbo].[CollectionAccessSelectionType] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
EXEC [dbo].[Group_Create] @Id, @OrganizationId, @Name, @AccessAll, @ExternalId, @CreationDate, @RevisionDate
|
||||
|
||||
;WITH [AvailableCollectionsCTE] AS(
|
||||
SELECT
|
||||
[Id]
|
||||
FROM
|
||||
[dbo].[Collection]
|
||||
WHERE
|
||||
[OrganizationId] = @OrganizationId
|
||||
)
|
||||
INSERT INTO [dbo].[CollectionGroup]
|
||||
(
|
||||
[CollectionId],
|
||||
[GroupId],
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
)
|
||||
SELECT
|
||||
[Id],
|
||||
@Id,
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
FROM
|
||||
@Collections
|
||||
WHERE
|
||||
[Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE])
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId
|
||||
END
|
@ -9,9 +9,10 @@ BEGIN
|
||||
SELECT
|
||||
[CollectionId] [Id],
|
||||
[ReadOnly],
|
||||
[HidePasswords]
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
FROM
|
||||
[dbo].[CollectionGroup]
|
||||
WHERE
|
||||
[GroupId] = @Id
|
||||
END
|
||||
END
|
||||
|
@ -23,14 +23,21 @@ BEGIN
|
||||
)
|
||||
MERGE
|
||||
[dbo].[CollectionGroup] AS [Target]
|
||||
USING
|
||||
USING
|
||||
@Collections AS [Source]
|
||||
ON
|
||||
[Target].[CollectionId] = [Source].[Id]
|
||||
AND [Target].[GroupId] = @Id
|
||||
WHEN NOT MATCHED BY TARGET
|
||||
AND [Source].[Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE]) THEN
|
||||
INSERT VALUES
|
||||
INSERT -- With column list because a value for Manage is not being provided
|
||||
(
|
||||
[CollectionId],
|
||||
[GroupId],
|
||||
[ReadOnly],
|
||||
[HidePasswords]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
[Source].[Id],
|
||||
@Id,
|
||||
@ -49,4 +56,4 @@ BEGIN
|
||||
;
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId
|
||||
END
|
||||
END
|
||||
|
@ -0,0 +1,63 @@
|
||||
CREATE PROCEDURE [dbo].[Group_UpdateWithCollections_V2]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Name NVARCHAR(100),
|
||||
@AccessAll BIT,
|
||||
@ExternalId NVARCHAR(300),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@Collections AS [dbo].[CollectionAccessSelectionType] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
EXEC [dbo].[Group_Update] @Id, @OrganizationId, @Name, @AccessAll, @ExternalId, @CreationDate, @RevisionDate
|
||||
|
||||
;WITH [AvailableCollectionsCTE] AS(
|
||||
SELECT
|
||||
Id
|
||||
FROM
|
||||
[dbo].[Collection]
|
||||
WHERE
|
||||
OrganizationId = @OrganizationId
|
||||
)
|
||||
MERGE
|
||||
[dbo].[CollectionGroup] AS [Target]
|
||||
USING
|
||||
@Collections AS [Source]
|
||||
ON
|
||||
[Target].[CollectionId] = [Source].[Id]
|
||||
AND [Target].[GroupId] = @Id
|
||||
WHEN NOT MATCHED BY TARGET
|
||||
AND [Source].[Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE]) THEN
|
||||
INSERT
|
||||
(
|
||||
[CollectionId],
|
||||
[GroupId],
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
[Source].[Id],
|
||||
@Id,
|
||||
[Source].[ReadOnly],
|
||||
[Source].[HidePasswords],
|
||||
[Source].[Manage]
|
||||
)
|
||||
WHEN MATCHED AND (
|
||||
[Target].[ReadOnly] != [Source].[ReadOnly]
|
||||
OR [Target].[HidePasswords] != [Source].[HidePasswords]
|
||||
OR [Target].[Manage] != [Source].[Manage]
|
||||
) THEN
|
||||
UPDATE SET [Target].[ReadOnly] = [Source].[ReadOnly],
|
||||
[Target].[HidePasswords] = [Source].[HidePasswords],
|
||||
[Target].[Manage] = [Source].[Manage]
|
||||
WHEN NOT MATCHED BY SOURCE
|
||||
AND [Target].[GroupId] = @Id THEN
|
||||
DELETE
|
||||
;
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrganizationId
|
||||
END
|
@ -9,11 +9,12 @@ BEGIN
|
||||
SELECT
|
||||
CU.[CollectionId] Id,
|
||||
CU.[ReadOnly],
|
||||
CU.[HidePasswords]
|
||||
CU.[HidePasswords],
|
||||
CU.[Manage]
|
||||
FROM
|
||||
[dbo].[OrganizationUser] OU
|
||||
INNER JOIN
|
||||
[dbo].[CollectionUser] CU ON OU.[AccessAll] = 0 AND CU.[OrganizationUserId] = [OU].[Id]
|
||||
WHERE
|
||||
[OrganizationUserId] = @Id
|
||||
END
|
||||
END
|
||||
|
@ -0,0 +1,49 @@
|
||||
CREATE PROCEDURE [dbo].[OrganizationUser_CreateWithCollections_V2]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@Email NVARCHAR(256),
|
||||
@Key VARCHAR(MAX),
|
||||
@Status SMALLINT,
|
||||
@Type TINYINT,
|
||||
@AccessAll BIT,
|
||||
@ExternalId NVARCHAR(300),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@Permissions NVARCHAR(MAX),
|
||||
@ResetPasswordKey VARCHAR(MAX),
|
||||
@Collections AS [dbo].[CollectionAccessSelectionType] READONLY,
|
||||
@AccessSecretsManager BIT = 0
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
EXEC [dbo].[OrganizationUser_Create] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate, @Permissions, @ResetPasswordKey, @AccessSecretsManager
|
||||
|
||||
;WITH [AvailableCollectionsCTE] AS(
|
||||
SELECT
|
||||
[Id]
|
||||
FROM
|
||||
[dbo].[Collection]
|
||||
WHERE
|
||||
[OrganizationId] = @OrganizationId
|
||||
)
|
||||
INSERT INTO [dbo].[CollectionUser]
|
||||
(
|
||||
[CollectionId],
|
||||
[OrganizationUserId],
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
)
|
||||
SELECT
|
||||
[Id],
|
||||
@Id,
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
FROM
|
||||
@Collections
|
||||
WHERE
|
||||
[Id] IN (SELECT [Id] FROM [AvailableCollectionsCTE])
|
||||
END
|
@ -36,9 +36,14 @@ BEGIN
|
||||
OR [Target].[HidePasswords] != [Source].[HidePasswords]
|
||||
)
|
||||
|
||||
-- Insert
|
||||
INSERT INTO
|
||||
[dbo].[CollectionUser]
|
||||
-- Insert (with column list because a value for Manage is not being provided)
|
||||
INSERT INTO [dbo].[CollectionUser]
|
||||
(
|
||||
[CollectionId],
|
||||
[OrganizationUserId],
|
||||
[ReadOnly],
|
||||
[HidePasswords]
|
||||
)
|
||||
SELECT
|
||||
[Source].[Id],
|
||||
@Id,
|
||||
@ -58,7 +63,7 @@ BEGIN
|
||||
[CollectionId] = [Source].[Id]
|
||||
AND [OrganizationUserId] = @Id
|
||||
)
|
||||
|
||||
|
||||
-- Delete
|
||||
DELETE
|
||||
CU
|
||||
|
@ -0,0 +1,86 @@
|
||||
CREATE PROCEDURE [dbo].[OrganizationUser_UpdateWithCollections_V2]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@Email NVARCHAR(256),
|
||||
@Key VARCHAR(MAX),
|
||||
@Status SMALLINT,
|
||||
@Type TINYINT,
|
||||
@AccessAll BIT,
|
||||
@ExternalId NVARCHAR(300),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@Permissions NVARCHAR(MAX),
|
||||
@ResetPasswordKey VARCHAR(MAX),
|
||||
@Collections AS [dbo].[CollectionAccessSelectionType] READONLY,
|
||||
@AccessSecretsManager BIT = 0
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
EXEC [dbo].[OrganizationUser_Update] @Id, @OrganizationId, @UserId, @Email, @Key, @Status, @Type, @AccessAll, @ExternalId, @CreationDate, @RevisionDate, @Permissions, @ResetPasswordKey, @AccessSecretsManager
|
||||
-- Update
|
||||
UPDATE
|
||||
[Target]
|
||||
SET
|
||||
[Target].[ReadOnly] = [Source].[ReadOnly],
|
||||
[Target].[HidePasswords] = [Source].[HidePasswords],
|
||||
[Target].[Manage] = [Source].[Manage]
|
||||
FROM
|
||||
[dbo].[CollectionUser] AS [Target]
|
||||
INNER JOIN
|
||||
@Collections AS [Source] ON [Source].[Id] = [Target].[CollectionId]
|
||||
WHERE
|
||||
[Target].[OrganizationUserId] = @Id
|
||||
AND (
|
||||
[Target].[ReadOnly] != [Source].[ReadOnly]
|
||||
OR [Target].[HidePasswords] != [Source].[HidePasswords]
|
||||
OR [Target].[Manage] != [Source].[Manage]
|
||||
)
|
||||
|
||||
-- Insert
|
||||
INSERT INTO [dbo].[CollectionUser]
|
||||
(
|
||||
[CollectionId],
|
||||
[OrganizationUserId],
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
)
|
||||
SELECT
|
||||
[Source].[Id],
|
||||
@Id,
|
||||
[Source].[ReadOnly],
|
||||
[Source].[HidePasswords],
|
||||
[Source].[Manage]
|
||||
FROM
|
||||
@Collections AS [Source]
|
||||
INNER JOIN
|
||||
[dbo].[Collection] C ON C.[Id] = [Source].[Id] AND C.[OrganizationId] = @OrganizationId
|
||||
WHERE
|
||||
NOT EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
[dbo].[CollectionUser]
|
||||
WHERE
|
||||
[CollectionId] = [Source].[Id]
|
||||
AND [OrganizationUserId] = @Id
|
||||
)
|
||||
|
||||
-- Delete
|
||||
DELETE
|
||||
CU
|
||||
FROM
|
||||
[dbo].[CollectionUser] CU
|
||||
WHERE
|
||||
CU.[OrganizationUserId] = @Id
|
||||
AND NOT EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
@Collections
|
||||
WHERE
|
||||
[Id] = CU.[CollectionId]
|
||||
)
|
||||
END
|
@ -50,7 +50,8 @@
|
||||
@SmServiceAccounts INT = null,
|
||||
@MaxAutoscaleSmSeats INT= null,
|
||||
@MaxAutoscaleSmServiceAccounts INT = null,
|
||||
@SecretsManagerBeta BIT = 0
|
||||
@SecretsManagerBeta BIT = 0,
|
||||
@LimitCollectionCreationDeletion BIT = 1
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@ -108,7 +109,8 @@ BEGIN
|
||||
[SmServiceAccounts],
|
||||
[MaxAutoscaleSmSeats],
|
||||
[MaxAutoscaleSmServiceAccounts],
|
||||
[SecretsManagerBeta]
|
||||
[SecretsManagerBeta],
|
||||
[LimitCollectionCreationDeletion]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@ -163,6 +165,7 @@ BEGIN
|
||||
@SmServiceAccounts,
|
||||
@MaxAutoscaleSmSeats,
|
||||
@MaxAutoscaleSmServiceAccounts,
|
||||
@SecretsManagerBeta
|
||||
@SecretsManagerBeta,
|
||||
@LimitCollectionCreationDeletion
|
||||
)
|
||||
END
|
||||
END
|
||||
|
@ -50,7 +50,8 @@
|
||||
@SmServiceAccounts INT = null,
|
||||
@MaxAutoscaleSmSeats INT = null,
|
||||
@MaxAutoscaleSmServiceAccounts INT = null,
|
||||
@SecretsManagerBeta BIT = 0
|
||||
@SecretsManagerBeta BIT = 0,
|
||||
@LimitCollectionCreationDeletion BIT = 1
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@ -108,7 +109,8 @@ BEGIN
|
||||
[SmServiceAccounts] = @SmServiceAccounts,
|
||||
[MaxAutoscaleSmSeats] = @MaxAutoscaleSmSeats,
|
||||
[MaxAutoscaleSmServiceAccounts] = @MaxAutoscaleSmServiceAccounts,
|
||||
[SecretsManagerBeta] = @SecretsManagerBeta
|
||||
[SecretsManagerBeta] = @SecretsManagerBeta,
|
||||
[LimitCollectionCreationDeletion] = @LimitCollectionCreationDeletion
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
|
@ -0,0 +1,35 @@
|
||||
CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByCollectionIds]
|
||||
@CollectionIds AS [dbo].[GuidIdArray] READONLY,
|
||||
@OrganizationId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
U
|
||||
SET
|
||||
U.[AccountRevisionDate] = GETUTCDATE()
|
||||
FROM
|
||||
[dbo].[User] U
|
||||
INNER JOIN
|
||||
[dbo].[Collection] C ON C.[Id] IN (SELECT [Id] FROM @CollectionIds)
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] OU ON OU.[UserId] = U.[Id]
|
||||
LEFT JOIN
|
||||
[dbo].[CollectionUser] CU ON OU.[AccessAll] = 0 AND CU.[OrganizationUserId] = OU.[Id] AND CU.[CollectionId] = C.[Id]
|
||||
LEFT JOIN
|
||||
[dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND OU.[AccessAll] = 0 AND GU.[OrganizationUserId] = OU.[Id]
|
||||
LEFT JOIN
|
||||
[dbo].[Group] G ON G.[Id] = GU.[GroupId]
|
||||
LEFT JOIN
|
||||
[dbo].[CollectionGroup] CG ON G.[AccessAll] = 0 AND CG.[GroupId] = GU.[GroupId] AND CG.[CollectionId] = C.[Id]
|
||||
WHERE
|
||||
OU.[OrganizationId] = @OrganizationId
|
||||
AND OU.[Status] = 2 -- 2 = Confirmed
|
||||
AND (
|
||||
CU.[CollectionId] IS NOT NULL
|
||||
OR CG.[CollectionId] IS NOT NULL
|
||||
OR OU.[AccessAll] = 1
|
||||
OR G.[AccessAll] = 1
|
||||
)
|
||||
END
|
@ -3,6 +3,7 @@
|
||||
[GroupId] UNIQUEIDENTIFIER NOT NULL,
|
||||
[ReadOnly] BIT NOT NULL,
|
||||
[HidePasswords] BIT NOT NULL,
|
||||
[Manage] BIT NOT NULL CONSTRAINT D_CollectionGroup_Manage DEFAULT (0),
|
||||
CONSTRAINT [PK_CollectionGroup] PRIMARY KEY CLUSTERED ([CollectionId] ASC, [GroupId] ASC),
|
||||
CONSTRAINT [FK_CollectionGroup_Collection] FOREIGN KEY ([CollectionId]) REFERENCES [dbo].[Collection] ([Id]),
|
||||
CONSTRAINT [FK_CollectionGroup_Group] FOREIGN KEY ([GroupId]) REFERENCES [dbo].[Group] ([Id]) ON DELETE CASCADE
|
||||
|
@ -3,6 +3,7 @@
|
||||
[OrganizationUserId] UNIQUEIDENTIFIER NOT NULL,
|
||||
[ReadOnly] BIT NOT NULL,
|
||||
[HidePasswords] BIT NOT NULL,
|
||||
[Manage] BIT NOT NULL CONSTRAINT D_CollectionUser_Manage DEFAULT (0),
|
||||
CONSTRAINT [PK_CollectionUser] PRIMARY KEY CLUSTERED ([CollectionId] ASC, [OrganizationUserId] ASC),
|
||||
CONSTRAINT [FK_CollectionUser_Collection] FOREIGN KEY ([CollectionId]) REFERENCES [dbo].[Collection] ([Id]) ON DELETE CASCADE,
|
||||
CONSTRAINT [FK_CollectionUser_OrganizationUser] FOREIGN KEY ([OrganizationUserId]) REFERENCES [dbo].[OrganizationUser] ([Id])
|
||||
|
@ -51,6 +51,7 @@
|
||||
[MaxAutoscaleSmSeats] INT NULL,
|
||||
[MaxAutoscaleSmServiceAccounts] INT NULL,
|
||||
[SecretsManagerBeta] BIT NOT NULL CONSTRAINT [DF_Organization_SecretsManagerBeta] DEFAULT (0),
|
||||
[LimitCollectionCreationDeletion] BIT NOT NULL CONSTRAINT [DF_Organization_LimitCollectionCreationDeletion] DEFAULT (1),
|
||||
CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||
);
|
||||
|
||||
|
@ -0,0 +1,6 @@
|
||||
CREATE TYPE [dbo].[CollectionAccessSelectionType] AS TABLE (
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||
[ReadOnly] BIT NOT NULL,
|
||||
[HidePasswords] BIT NOT NULL,
|
||||
[Manage] BIT NOT NULL);
|
||||
|
@ -44,7 +44,8 @@ SELECT
|
||||
OU.[AccessSecretsManager],
|
||||
O.[UsePasswordManager],
|
||||
O.[SmSeats],
|
||||
O.[SmServiceAccounts]
|
||||
O.[SmServiceAccounts],
|
||||
O.[LimitCollectionCreationDeletion]
|
||||
FROM
|
||||
[dbo].[OrganizationUser] OU
|
||||
LEFT JOIN
|
||||
|
Reference in New Issue
Block a user