1
0
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:
Thomas Rittson
2023-11-01 19:30:52 +10:00
committed by GitHub
parent 419760623a
commit da4a86c643
104 changed files with 18289 additions and 256 deletions

View File

@ -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);
}
}

View File

@ -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

View File

@ -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; }
}

View File

@ -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;
}

View File

@ -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; }
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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,
};
}
}

View File

@ -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

View File

@ -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; }
}

View File

@ -137,6 +137,9 @@ public class Startup
services.AddOrganizationSubscriptionServices();
services.AddCoreLocalizationServices();
// Authorization Handlers
services.AddAuthorizationHandlers();
//health check
if (!globalSettings.SelfHosted)
{

View File

@ -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>();
}
}

View File

@ -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);
}
}

View File

@ -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) };
}

View File

@ -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";

View File

@ -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))

View File

@ -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; }
}

View File

@ -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);
}

View File

@ -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; }
}

View File

@ -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; }
}

View File

@ -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()
{

View File

@ -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; }
}

View File

@ -6,4 +6,5 @@ public class CollectionDetails : Collection
{
public bool ReadOnly { get; set; }
public bool HidePasswords { get; set; }
public bool Manage { get; set; }
}

View File

@ -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; }
}

View File

@ -142,6 +142,7 @@ public class SelfHostedOrganizationDetails : Organization
RevisionDate = RevisionDate,
MaxAutoscaleSeats = MaxAutoscaleSeats,
OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,
LimitCollectionCreationDeletion = LimitCollectionCreationDeletion,
};
}
}

View File

@ -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.");
}
}
}
}

View File

@ -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);
}

View File

@ -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)

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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;
}

View 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);
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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(

View File

@ -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();

View File

@ -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

View File

@ -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);
}

View File

@ -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,
});
}
}

View File

@ -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

View File

@ -7,9 +7,10 @@ BEGIN
SELECT
[GroupId] [Id],
[ReadOnly],
[HidePasswords]
[HidePasswords],
[Manage]
FROM
[dbo].[CollectionGroup]
WHERE
[CollectionId] = @CollectionId
END
END

View File

@ -7,9 +7,10 @@ BEGIN
SELECT
[OrganizationUserId] [Id],
[ReadOnly],
[HidePasswords]
[HidePasswords],
[Manage]
FROM
[dbo].[CollectionUser]
WHERE
[CollectionId] = @CollectionId
END
END

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -9,9 +9,10 @@ BEGIN
SELECT
[CollectionId] [Id],
[ReadOnly],
[HidePasswords]
[HidePasswords],
[Manage]
FROM
[dbo].[CollectionGroup]
WHERE
[GroupId] = @Id
END
END

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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])

View File

@ -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)
);

View File

@ -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);

View File

@ -44,7 +44,8 @@ SELECT
OU.[AccessSecretsManager],
O.[UsePasswordManager],
O.[SmSeats],
O.[SmServiceAccounts]
O.[SmServiceAccounts],
O.[LimitCollectionCreationDeletion]
FROM
[dbo].[OrganizationUser] OU
LEFT JOIN