1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-08 00:31:27 +01:00

SqlServer split manage collection permission (#1594)

* SqlServer split manage collection permission

* Clarify names

* Test claims generation

* Test permission serialization

* Simplify claims building

* Use new collections permissions

* Throw on use of deprecated permissions

* Lower case all claims

* Remove todos

* Clean nonexistent project from test solution

* JsonIgnore for both system and newtonsoft json

* Make migrations more robust to multiple runs

* remove duplicate usings

* Remove obsolete permissions

* Test solutions separately to detect failures

* Handle dos line endings

* Fix collections create/update permissions

* Change restore cipher to edit permissions

* Improve formatting

* Simplify map

* Refactor test
This commit is contained in:
Matt Gibson 2021-10-05 11:12:05 -05:00 committed by GitHub
parent 55fa4a5f63
commit bd297fb7a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 3639 additions and 129 deletions

View File

@ -68,10 +68,12 @@ jobs:
run: msbuild bitwarden-server.sln /p:Configuration=Debug /verbosity:minimal
shell: pwsh
- name: Test solution
run: |
dotnet test ./test --configuration Debug --no-build
dotnet test ./bitwarden_license/test/CmmCore.Test --configuration Debug --no-build
- name: Test OSS solution
run: dotnet test ./test --configuration Debug --no-build
shell: pwsh
- name: Test Bitwarden solution
run: dotnet test ./bitwarden_license/test/CmmCore.Test --configuration Debug --no-build
shell: pwsh

View File

@ -76,7 +76,7 @@ namespace Bit.Api.Controllers
{
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!await _currentContext.ManageAllCollections(cipher.OrganizationId.Value))
!await _currentContext.ViewAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
@ -153,7 +153,7 @@ namespace Bit.Api.Controllers
public async Task<CipherMiniResponseModel> PostAdmin([FromBody]CipherCreateRequestModel model)
{
var cipher = model.Cipher.ToOrganizationCipher();
if (!await _currentContext.ManageAllCollections(cipher.OrganizationId.Value))
if (!await _currentContext.EditAnyCollection(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
@ -197,7 +197,7 @@ namespace Bit.Api.Controllers
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!await _currentContext.ManageAllCollections(cipher.OrganizationId.Value))
!await _currentContext.EditAnyCollection(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
@ -216,7 +216,7 @@ namespace Bit.Api.Controllers
{
var userId = _userService.GetProperUserId(User).Value;
var orgIdGuid = new Guid(organizationId);
if (!await _currentContext.ManageAllCollections(orgIdGuid) && !await _currentContext.AccessReports(orgIdGuid))
if (!await _currentContext.ViewAllCollections(orgIdGuid) && !await _currentContext.AccessReports(orgIdGuid))
{
throw new NotFoundException();
}
@ -330,7 +330,7 @@ namespace Bit.Api.Controllers
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!await _currentContext.ManageAllCollections(cipher.OrganizationId.Value))
!await _currentContext.EditAnyCollection(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
@ -360,7 +360,7 @@ namespace Bit.Api.Controllers
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!await _currentContext.ManageAllCollections(cipher.OrganizationId.Value))
!await _currentContext.EditAnyCollection(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
@ -393,7 +393,7 @@ namespace Bit.Api.Controllers
}
if (model == null || string.IsNullOrWhiteSpace(model.OrganizationId) ||
!await _currentContext.ManageAllCollections(new Guid(model.OrganizationId)))
!await _currentContext.EditAnyCollection(new Guid(model.OrganizationId)))
{
throw new NotFoundException();
}
@ -420,7 +420,7 @@ namespace Bit.Api.Controllers
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!await _currentContext.ManageAllCollections(cipher.OrganizationId.Value))
!await _currentContext.EditAnyCollection(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
@ -449,7 +449,7 @@ namespace Bit.Api.Controllers
}
if (model == null || string.IsNullOrWhiteSpace(model.OrganizationId) ||
!await _currentContext.ManageAllCollections(new Guid(model.OrganizationId)))
!await _currentContext.EditAnyCollection(new Guid(model.OrganizationId)))
{
throw new NotFoundException();
}
@ -478,7 +478,7 @@ namespace Bit.Api.Controllers
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!await _currentContext.ManageAllCollections(cipher.OrganizationId.Value))
!await _currentContext.EditAnyCollection(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
@ -572,7 +572,7 @@ namespace Bit.Api.Controllers
else
{
var orgId = new Guid(organizationId);
if (!await _currentContext.ManageAllCollections(orgId))
if (!await _currentContext.EditAnyCollection(orgId))
{
throw new NotFoundException();
}
@ -590,7 +590,7 @@ namespace Bit.Api.Controllers
await _cipherRepository.GetByIdAsync(idGuid, userId);
if (cipher == null || (request.AdminRequest && (!cipher.OrganizationId.HasValue ||
!await _currentContext.ManageAllCollections(cipher.OrganizationId.Value))))
!await _currentContext.EditAnyCollection(cipher.OrganizationId.Value))))
{
throw new NotFoundException();
}
@ -694,7 +694,7 @@ namespace Bit.Api.Controllers
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(idGuid);
if (cipher == null || !cipher.OrganizationId.HasValue ||
!await _currentContext.ManageAllCollections(cipher.OrganizationId.Value))
!await _currentContext.EditAnyCollection(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
@ -760,7 +760,7 @@ namespace Bit.Api.Controllers
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(idGuid);
if (cipher == null || !cipher.OrganizationId.HasValue ||
!await _currentContext.ManageAllCollections(cipher.OrganizationId.Value))
!await _currentContext.EditAnyCollection(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}

View File

@ -37,6 +37,11 @@ namespace Bit.Api.Controllers
[HttpGet("{id}")]
public async Task<CollectionResponseModel> Get(string orgId, string id)
{
if (!await CanViewCollectionAsync(orgId, id))
{
throw new NotFoundException();
}
var collection = await GetCollectionAsync(new Guid(id), new Guid(orgId));
return new CollectionResponseModel(collection);
}
@ -45,13 +50,13 @@ namespace Bit.Api.Controllers
public async Task<CollectionGroupDetailsResponseModel> GetDetails(string orgId, string id)
{
var orgIdGuid = new Guid(orgId);
if (!await ManageAnyCollections(orgIdGuid) && !await _currentContext.ManageUsers(orgIdGuid))
if (!await ViewAtLeastOneCollectionAsync(orgIdGuid) && !await _currentContext.ManageUsers(orgIdGuid))
{
throw new NotFoundException();
}
var idGuid = new Guid(id);
if (await _currentContext.ManageAllCollections(orgIdGuid))
if (await _currentContext.ViewAllCollections(orgIdGuid))
{
var collectionDetails = await _collectionRepository.GetByIdWithGroupsAsync(idGuid);
if (collectionDetails?.Item1 == null || collectionDetails.Item1.OrganizationId != orgIdGuid)
@ -76,7 +81,7 @@ namespace Bit.Api.Controllers
public async Task<ListResponseModel<CollectionResponseModel>> Get(string orgId)
{
var orgIdGuid = new Guid(orgId);
if (!await _currentContext.ManageAllCollections(orgIdGuid) && !await _currentContext.ManageUsers(orgIdGuid))
if (!await _currentContext.ViewAllCollections(orgIdGuid) && !await _currentContext.ManageUsers(orgIdGuid))
{
throw new NotFoundException();
}
@ -108,14 +113,16 @@ namespace Bit.Api.Controllers
public async Task<CollectionResponseModel> Post(string orgId, [FromBody]CollectionRequestModel model)
{
var orgIdGuid = new Guid(orgId);
if (!await ManageAnyCollections(orgIdGuid))
var collection = model.ToCollection(orgIdGuid);
if (!await CanCreateCollection(orgIdGuid, collection.Id) &&
!await CanEditCollectionAsync(orgIdGuid, collection.Id))
{
throw new NotFoundException();
}
var collection = model.ToCollection(orgIdGuid);
await _collectionService.SaveAsync(collection, model.Groups?.Select(g => g.ToSelectionReadOnly()),
!await _currentContext.ManageAllCollections(orgIdGuid) ? _currentContext.UserId : null);
!await _currentContext.ViewAllCollections(orgIdGuid) ? _currentContext.UserId : null);
return new CollectionResponseModel(collection);
}
@ -123,6 +130,11 @@ namespace Bit.Api.Controllers
[HttpPost("{id}")]
public async Task<CollectionResponseModel> Put(string orgId, string id, [FromBody]CollectionRequestModel model)
{
if (!await CanEditCollectionAsync(orgId, id))
{
throw new NotFoundException();
}
var collection = await GetCollectionAsync(new Guid(id), new Guid(orgId));
await _collectionService.SaveAsync(model.ToCollection(collection),
model.Groups?.Select(g => g.ToSelectionReadOnly()));
@ -140,6 +152,11 @@ namespace Bit.Api.Controllers
[HttpPost("{id}/delete")]
public async Task Delete(string orgId, string id)
{
if (!await CanDeleteCollectionAsync(orgId, id))
{
throw new NotFoundException();
}
var collection = await GetCollectionAsync(new Guid(id), new Guid(orgId));
await _collectionService.DeleteAsync(collection);
}
@ -154,14 +171,17 @@ namespace Bit.Api.Controllers
private async Task<Collection> GetCollectionAsync(Guid id, Guid orgId)
{
if (!await ManageAnyCollections(orgId))
Collection collection = default;
if (await _currentContext.ViewAllCollections(orgId))
{
throw new NotFoundException();
collection = await _collectionRepository.GetByIdAsync(id);
}
if (await _currentContext.ViewAssignedCollections(orgId))
{
collection = await _collectionRepository.GetByIdAsync(id, _currentContext.UserId.Value);
}
var collection = await _currentContext.OrganizationAdmin(orgId) ?
await _collectionRepository.GetByIdAsync(id) :
await _collectionRepository.GetByIdAsync(id, _currentContext.UserId.Value);
if (collection == null || collection.OrganizationId != orgId)
{
throw new NotFoundException();
@ -170,9 +190,86 @@ namespace Bit.Api.Controllers
return collection;
}
private async Task<bool> ManageAnyCollections(Guid orgId)
public async Task<bool> CanCreateCollection(Guid orgId, Guid collectionId)
{
return await _currentContext.ManageAssignedCollections(orgId) || await _currentContext.ManageAllCollections(orgId);
if (collectionId != default)
{
return false;
}
return await _currentContext.CreateNewCollections(orgId);
}
private async Task<bool> CanEditCollectionAsync(string orgId, string collectionId) =>
await CanEditCollectionAsync(new Guid(orgId), new Guid(collectionId));
private async Task<bool> CanEditCollectionAsync(Guid orgId, Guid collectionId)
{
if (collectionId == default)
{
return false;
}
if (await _currentContext.EditAnyCollection(orgId))
{
return true;
}
if (await _currentContext.EditAssignedCollections(orgId))
{
return null != _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value);
}
return false;
}
private async Task<bool> CanDeleteCollectionAsync(string orgId, string collectionId) =>
await CanDeleteCollectionAsync(new Guid(orgId), new Guid(collectionId));
private async Task<bool> CanDeleteCollectionAsync(Guid orgId, Guid collectionId)
{
if (collectionId == default)
{
return false;
}
if (await _currentContext.DeleteAnyCollection(orgId))
{
return true;
}
if (await _currentContext.DeleteAssignedCollections(orgId))
{
return null != _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value);
}
return false;
}
private async Task<bool> CanViewCollectionAsync(string orgId, string collectionId) =>
await CanViewCollectionAsync(new Guid(orgId), new Guid(collectionId));
private async Task<bool> CanViewCollectionAsync(Guid orgId, Guid collectionId)
{
if (collectionId == default)
{
return false;
}
if (await _currentContext.ViewAllCollections(orgId))
{
return true;
}
if (await _currentContext.ViewAssignedCollections(orgId))
{
return null != _collectionRepository.GetByIdAsync(collectionId, _currentContext.UserId.Value);
}
return false;
}
private async Task<bool> ViewAtLeastOneCollectionAsync(Guid orgId)
{
return await _currentContext.ViewAllCollections(orgId) || await _currentContext.ViewAssignedCollections(orgId);
}
}
}

View File

@ -59,8 +59,8 @@ namespace Bit.Api.Controllers
{
var orgIdGuid = new Guid(orgId);
var canAccess = await _currentContext.ManageGroups(orgIdGuid) ||
await _currentContext.ManageAssignedCollections(orgIdGuid) ||
await _currentContext.ManageAllCollections(orgIdGuid) ||
await _currentContext.ViewAssignedCollections(orgIdGuid) ||
await _currentContext.ViewAllCollections(orgIdGuid) ||
await _currentContext.ManageUsers(orgIdGuid);
if (!canAccess)

View File

@ -61,7 +61,7 @@ namespace Bit.Api.Controllers
public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get(string orgId)
{
var orgGuidId = new Guid(orgId);
if (!await _currentContext.ManageAssignedCollections(orgGuidId) &&
if (!await _currentContext.ViewAssignedCollections(orgGuidId) &&
!await _currentContext.ManageGroups(orgGuidId) &&
!await _currentContext.ManageUsers(orgGuidId))
{

View File

@ -142,7 +142,7 @@ namespace Bit.Core.Context
Organizations = GetOrganizations(claimsDict, orgApi);
Providers = GetProviders(claimsDict);
return Task.FromResult(0);
}
@ -210,7 +210,7 @@ namespace Bit.Core.Context
return organizations;
}
private List<CurrentContentProvider> GetProviders(Dictionary<string, IEnumerable<Claim>> claimsDict)
{
var providers = new List<CurrentContentProvider>();
@ -274,6 +274,7 @@ namespace Bit.Core.Context
return Task.FromResult(Organizations?.Any(o => o.Id == orgId && o.Type == OrganizationUserType.Custom) ?? false);
}
public async Task<bool> AccessBusinessPortal(Guid orgId)
{
return await OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
@ -298,16 +299,44 @@ namespace Bit.Core.Context
&& (o.Permissions?.AccessReports ?? false)) ?? false);
}
public async Task<bool> ManageAllCollections(Guid orgId)
public async Task<bool> CreateNewCollections(Guid orgId)
{
return await OrganizationAdmin(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.ManageAllCollections ?? false)) ?? false);
&& (o.Permissions?.CreateNewCollections ?? false)) ?? false);
}
public async Task<bool> ManageAssignedCollections(Guid orgId)
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);
}
public async Task<bool> EditAssignedCollections(Guid orgId)
{
return await OrganizationManager(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.ManageAssignedCollections ?? false)) ?? false);
&& (o.Permissions?.EditAssignedCollections ?? false)) ?? false);
}
public async Task<bool> DeleteAssignedCollections(Guid orgId)
{
return await OrganizationManager(orgId) || (Organizations?.Any(o => o.Id == orgId
&& (o.Permissions?.DeleteAssignedCollections ?? false)) ?? false);
}
public async Task<bool> ViewAssignedCollections(Guid orgId)
{
return await EditAssignedCollections(orgId) || await DeleteAssignedCollections(orgId);
}
public async Task<bool> ManageGroups(Guid orgId)
@ -431,8 +460,11 @@ namespace Bit.Core.Context
AccessEventLogs = hasClaim("accesseventlogs"),
AccessImportExport = hasClaim("accessimportexport"),
AccessReports = hasClaim("accessreports"),
ManageAllCollections = hasClaim("manageallcollections"),
ManageAssignedCollections = hasClaim("manageassignedcollections"),
CreateNewCollections = hasClaim("createnewcollections"),
EditAnyCollection = hasClaim("editanycollection"),
DeleteAnyCollection = hasClaim("deleteanycollection"),
EditAssignedCollections = hasClaim("editassignedcollections"),
DeleteAssignedCollections = hasClaim("deleteassignedcollections"),
ManageGroups = hasClaim("managegroups"),
ManagePolicies = hasClaim("managepolicies"),
ManageSso = hasClaim("managesso"),

View File

@ -40,8 +40,13 @@ namespace Bit.Core.Context
Task<bool> AccessEventLogs(Guid orgId);
Task<bool> AccessImportExport(Guid orgId);
Task<bool> AccessReports(Guid orgId);
Task<bool> ManageAllCollections(Guid orgId);
Task<bool> ManageAssignedCollections(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);
Task<bool> ViewAssignedCollections(Guid orgId);
Task<bool> ManageGroups(Guid orgId);
Task<bool> ManagePolicies(Guid orgId);
Task<bool> ManageSso(Guid orgId);

View File

@ -1,3 +1,7 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Bit.Core.Models.Data
{
public class Permissions
@ -6,12 +10,39 @@ namespace Bit.Core.Models.Data
public bool AccessEventLogs { get; set; }
public bool AccessImportExport { get; set; }
public bool AccessReports { get; set; }
public bool ManageAssignedCollections { get; set; }
public bool ManageAllCollections { get; set; }
[Obsolete("This permission exists for client backwards-compatibility. It should not be used to determine permissions in this repository", true)]
public bool ManageAllCollections => CreateNewCollections && EditAnyCollection && DeleteAnyCollection;
public bool CreateNewCollections { get; set; }
public bool EditAnyCollection { get; set; }
public bool DeleteAnyCollection { get; set; }
[Obsolete("This permission exists for client backwards-compatibility. It should not be used to determine permissions in this repository", true)]
public bool ManageAssignedCollections => EditAssignedCollections && DeleteAssignedCollections;
public bool EditAssignedCollections { get; set; }
public bool DeleteAssignedCollections { get; set; }
public bool ManageGroups { get; set; }
public bool ManagePolicies { get; set; }
public bool ManageSso { get; set; }
public bool ManageUsers { get; set; }
public bool ManageResetPassword { get; set; }
[JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public List<(bool Permission, string ClaimName)> ClaimsMap => new()
{
(AccessBusinessPortal, "accessbusinessportal"),
(AccessEventLogs, "accesseventlogs"),
(AccessImportExport, "accessimportexport"),
(AccessReports, "accessreports"),
(CreateNewCollections, "createnewcollections"),
(EditAnyCollection, "editanycollection"),
(DeleteAnyCollection, "deleteanycollection"),
(EditAssignedCollections, "editassignedcollections"),
(DeleteAssignedCollections, "deleteassignedcollections"),
(ManageGroups, "managegroups"),
(ManagePolicies, "managepolicies"),
(ManageSso, "managesso"),
(ManageUsers, "manageusers"),
(ManageResetPassword, "manageresetpassword"),
};
}
}

View File

@ -242,6 +242,17 @@ namespace Bit.Core.Utilities
}
}
public static string GetEmbeddedResourceContentsAsync(string file)
{
var assembly = Assembly.GetCallingAssembly();
var resourceName = assembly.GetManifestResourceNames().Single(n => n.EndsWith(file));
using (var stream = assembly.GetManifestResourceStream(resourceName))
using (var reader = new StreamReader(stream))
{
return reader.ReadToEnd();
}
}
public async static Task<X509Certificate2> GetBlobCertificateAsync(CloudStorageAccount cloudStorageAccount,
string container, string file, string password)
{
@ -827,60 +838,14 @@ namespace Bit.Core.Utilities
foreach (var org in group)
{
claims.Add(new KeyValuePair<string, string>("orgcustom", org.Id.ToString()));
foreach (var (permission, claimName) in org.Permissions.ClaimsMap)
{
if (!permission)
{
continue;
}
if (org.Permissions.AccessBusinessPortal)
{
claims.Add(new KeyValuePair<string, string>("accessbusinessportal", org.Id.ToString()));
}
if (org.Permissions.AccessEventLogs)
{
claims.Add(new KeyValuePair<string, string>("accesseventlogs", org.Id.ToString()));
}
if (org.Permissions.AccessImportExport)
{
claims.Add(new KeyValuePair<string, string>("accessimportexport", org.Id.ToString()));
}
if (org.Permissions.AccessReports)
{
claims.Add(new KeyValuePair<string, string>("accessreports", org.Id.ToString()));
}
if (org.Permissions.ManageAllCollections)
{
claims.Add(new KeyValuePair<string, string>("manageallcollections", org.Id.ToString()));
}
if (org.Permissions.ManageAssignedCollections)
{
claims.Add(new KeyValuePair<string, string>("manageassignedcollections", org.Id.ToString()));
}
if (org.Permissions.ManageGroups)
{
claims.Add(new KeyValuePair<string, string>("managegroups", org.Id.ToString()));
}
if (org.Permissions.ManagePolicies)
{
claims.Add(new KeyValuePair<string, string>("managepolicies", org.Id.ToString()));
}
if (org.Permissions.ManageSso)
{
claims.Add(new KeyValuePair<string, string>("managesso", org.Id.ToString()));
}
if (org.Permissions.ManageUsers)
{
claims.Add(new KeyValuePair<string, string>("manageusers", org.Id.ToString()));
}
if (org.Permissions.ManageResetPassword)
{
claims.Add(new KeyValuePair<string, string>("manageresetpassword", org.Id.ToString()));
claims.Add(new KeyValuePair<string, string>(claimName, org.Id.ToString()));
}
}
break;

View File

@ -4,17 +4,11 @@ using AutoFixture;
using TableModel = Bit.Core.Models.Table;
using Bit.Core.Test.AutoFixture.Attributes;
using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures;
using AutoMapper;
using Bit.Core.Models.EntityFramework;
using Bit.Core.Models;
using System.Collections.Generic;
using Bit.Core.Enums;
using AutoFixture.Kernel;
using System;
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using Bit.Core.Repositories.EntityFramework;
using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures;
using System.Reflection;
using AutoFixture.Xunit2;
namespace Bit.Core.Test.AutoFixture.PolicyFixtures

View File

@ -2,8 +2,6 @@ using AutoFixture;
using TableModel = Bit.Core.Models.Table;
using Bit.Core.Test.AutoFixture.Attributes;
using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures;
using AutoMapper;
using Bit.Core.Models.EntityFramework;
using Bit.Core.Models;
using System.Collections.Generic;
using Bit.Core.Enums;
@ -15,11 +13,11 @@ using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures;
namespace Bit.Core.Test.AutoFixture.UserFixtures
{
internal class UserBuilder: ISpecimenBuilder
internal class UserBuilder : ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
if (context == null)
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
@ -49,19 +47,27 @@ namespace Bit.Core.Test.AutoFixture.UserFixtures
}
}
internal class EfUser: ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customizations.Add(new IgnoreVirtualMembersCustomization());
fixture.Customizations.Add(new GlobalSettingsBuilder());
fixture.Customizations.Add(new UserBuilder());
fixture.Customizations.Add(new OrganizationBuilder());
fixture.Customizations.Add(new EfRepositoryListBuilder<UserRepository>());
fixture.Customizations.Add(new EfRepositoryListBuilder<SsoUserRepository>());
fixture.Customizations.Add(new EfRepositoryListBuilder<OrganizationRepository>());
}
}
internal class UserFixture : ICustomization
{
public virtual void Customize(IFixture fixture)
{
fixture.Customizations.Add(new IgnoreVirtualMembersCustomization());
fixture.Customizations.Add(new GlobalSettingsBuilder());
fixture.Customizations.Add(new UserBuilder());
fixture.Customizations.Add(new OrganizationBuilder());
}
}
internal class EfUser : UserFixture
{
public override void Customize(IFixture fixture)
{
base.Customize(fixture);
fixture.Customizations.Add(new EfRepositoryListBuilder<UserRepository>());
fixture.Customizations.Add(new EfRepositoryListBuilder<SsoUserRepository>());
fixture.Customizations.Add(new EfRepositoryListBuilder<OrganizationRepository>());
}
}
internal class EfUserAutoDataAttribute : CustomAutoDataAttribute
{

View File

@ -26,4 +26,10 @@
<ProjectReference Include="..\..\src\Core\Core.csproj" />
<ProjectReference Include="..\..\src\Api\Api.csproj" />
</ItemGroup>
<ItemGroup>
<None Remove="Utilities\data\embeddedResource.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Utilities\data\embeddedResource.txt" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,56 @@
using System;
using System.Text.Json;
using AutoFixture.Xunit2;
using Bit.Core.Models.Data;
using Bit.Core.Models.Table;
using Bit.Core.Utilities;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Xunit;
namespace Bit.Core.Test.Models
{
public class PermissionsTests
{
private static readonly string _exampleSerializedPermissions = string.Concat(
"{",
"\"accessBusinessPortal\": false,",
"\"accessEventLogs\": false,",
"\"accessImportExport\": false,",
"\"accessReports\": false,",
"\"manageAllCollections\": true,", // exists for backwards compatibility
"\"createNewCollections\": true,",
"\"editAnyCollection\": true,",
"\"deleteAnyCollection\": true,",
"\"manageAssignedCollections\": false,", // exists for backwards compatibility
"\"editAssignedCollections\": false,",
"\"deleteAssignedCollections\": false,",
"\"manageGroups\": false,",
"\"managePolicies\": false,",
"\"manageSso\": false,",
"\"manageUsers\": false,",
"\"manageResetPassword\": false",
"}");
[Fact]
public void Serialization_Success()
{
// minify expected json
var expected = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(_exampleSerializedPermissions));
DefaultContractResolver contractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy()
};
var actual = JsonConvert.SerializeObject(
CoreHelpers.LoadClassFromJsonData<Permissions>(_exampleSerializedPermissions), new JsonSerializerSettings
{
ContractResolver = contractResolver,
});
Console.WriteLine(actual);
Assert.Equal(expected, actual);
}
}
}

View File

@ -1,8 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Bit.Core.Models.Table;
using Bit.Core.Repositories.EntityFramework;
using Bit.Core.Test.AutoFixture;
using Bit.Core.Test.AutoFixture.Attributes;
using Bit.Core.Test.AutoFixture.CipherFixtures;
using Bit.Core.Test.Repositories.EntityFramework.EqualityComparers;
@ -10,7 +8,6 @@ using Microsoft.EntityFrameworkCore;
using Xunit;
using EfRepo = Bit.Core.Repositories.EntityFramework;
using SqlRepo = Bit.Core.Repositories.SqlServer;
using Bit.Core.Test.AutoFixture.CipherFixtures;
using Bit.Core.Repositories.EntityFramework.Queries;
using Bit.Core.Models.Data;
using System;

View File

@ -3,7 +3,15 @@ using System.Collections.Generic;
using System.Linq;
using Bit.Core.Utilities;
using Xunit;
using MimeKit;
using Bit.Core.Test.AutoFixture.Attributes;
using Bit.Core.Test.AutoFixture.UserFixtures;
using IdentityModel;
using Bit.Core.Enums.Provider;
using Bit.Core.Models.Table;
using Bit.Core.Context;
using AutoFixture;
using Bit.Core.Test.AutoFixture;
using Bit.Core.Enums;
namespace Bit.Core.Test.Utilities
{
@ -150,11 +158,11 @@ namespace Bit.Core.Test.Utilities
[Fact]
public void CloneObject_Success()
{
var orignial = new { Message = "Message" };
var original = new { Message = "Message" };
var copy = CoreHelpers.CloneObject(orignial);
var copy = CoreHelpers.CloneObject(original);
Assert.Equal(orignial.Message, copy.Message);
Assert.Equal(original.Message, copy.Message);
}
[Fact]
@ -232,5 +240,116 @@ namespace Bit.Core.Test.Utilities
var actual = CoreHelpers.PunyEncode(text);
Assert.Equal(expected, actual);
}
[Fact]
public void GetEmbeddedResourceContentsAsync_Success()
{
var fileContents = CoreHelpers.GetEmbeddedResourceContentsAsync("data.embeddedResource.txt");
Assert.Equal("Contents of embeddedResource.txt\n", fileContents.Replace("\r\n", "\n"));
}
[Theory, CustomAutoData(typeof(UserFixture))]
public void BuildIdentityClaims_BaseClaims_Success(User user, bool isPremium)
{
var expected = new Dictionary<string, string>
{
{ "premium", isPremium ? "true" : "false" },
{ JwtClaimTypes.Email, user.Email },
{ JwtClaimTypes.EmailVerified, user.EmailVerified ? "true" : "false" },
{ JwtClaimTypes.Name, user.Name },
{ "sstamp", user.SecurityStamp },
}.ToList();
var actual = CoreHelpers.BuildIdentityClaims(user, Array.Empty<CurrentContentOrganization>(),
Array.Empty<CurrentContentProvider>(), isPremium);
foreach (var claim in expected)
{
Assert.Contains(claim, actual);
}
Assert.Equal(expected.Count, actual.Count);
}
[Theory, CustomAutoData(typeof(UserFixture))]
public void BuildIdentityClaims_NonCustomOrganizationUserType_Success(User user)
{
var fixture = new Fixture().WithAutoNSubstitutions();
foreach (var organizationUserType in Enum.GetValues<OrganizationUserType>().Except(new[] { OrganizationUserType.Custom }))
{
var org = fixture.Create<CurrentContentOrganization>();
org.Type = organizationUserType;
var expected = new KeyValuePair<string, string>($"org{organizationUserType.ToString().ToLower()}", org.Id.ToString());
var actual = CoreHelpers.BuildIdentityClaims(user, new[] { org }, Array.Empty<CurrentContentProvider>(), false);
Assert.Contains(expected, actual);
}
}
[Theory, CustomAutoData(typeof(UserFixture))]
public void BuildIdentityClaims_CustomOrganizationUserClaims_Success(User user, CurrentContentOrganization org)
{
var fixture = new Fixture().WithAutoNSubstitutions();
org.Type = OrganizationUserType.Custom;
var actual = CoreHelpers.BuildIdentityClaims(user, new[] { org }, Array.Empty<CurrentContentProvider>(), false);
foreach (var (permitted, claimName) in org.Permissions.ClaimsMap)
{
var claim = new KeyValuePair<string, string>(claimName, org.Id.ToString());
if (permitted)
{
Assert.Contains(claim, actual);
}
else
{
Assert.DoesNotContain(claim, actual);
}
}
}
[Theory, CustomAutoData(typeof(UserFixture))]
public void BuildIdentityClaims_ProviderClaims_Success(User user)
{
var fixture = new Fixture().WithAutoNSubstitutions();
var providers = new List<CurrentContentProvider>();
foreach (var providerUserType in Enum.GetValues<ProviderUserType>())
{
var provider = fixture.Create<CurrentContentProvider>();
provider.Type = providerUserType;
providers.Add(provider);
}
var claims = new List<KeyValuePair<string, string>>();
if (providers.Any())
{
foreach (var group in providers.GroupBy(o => o.Type))
{
switch (group.Key)
{
case ProviderUserType.ProviderAdmin:
foreach (var provider in group)
{
claims.Add(new KeyValuePair<string, string>("providerprovideradmin", provider.Id.ToString()));
}
break;
case ProviderUserType.ServiceUser:
foreach (var provider in group)
{
claims.Add(new KeyValuePair<string, string>("providerserviceuser", provider.Id.ToString()));
}
break;
}
}
}
var actual = CoreHelpers.BuildIdentityClaims(user, Array.Empty<CurrentContentOrganization>(), providers, false);
foreach (var claim in claims)
{
Assert.Contains(claim, actual);
}
}
}
}

View File

@ -0,0 +1 @@
Contents of embeddedResource.txt

View File

@ -0,0 +1,64 @@
-- Split Manage Assigned Collections into edit and delete
UPDATE [vault_dev].[dbo].[OrganizationUser]
SET [Permissions] =
JSON_MODIFY(
JSON_MODIFY(
[Permissions],
'$.editAssignedCollections',
CAST(ISNULL(
ISNULL(
JSON_VALUE([Permissions], '$.editAssignedCollections'),
JSON_VALUE([Permissions], '$.manageAssignedCollections')
),
0) AS BIT)
),
'$.deleteAssignedCollections',
CAST(ISNULL(
ISNULL(
JSON_VALUE([Permissions], '$.deleteAssignedCollections'),
JSON_VALUE([Permissions], '$.manageAssignedCollections')),
0) AS BIT)
)
WHERE [Permissions] IS NOT NULL
AND ISJSON([Permissions]) > 0
AND (
JSON_VALUE([Permissions], '$.editAssignedCollections') IS NULL
OR JSON_VALUE([Permissions], '$.deleteAssignedCollections') IS NULL
)
-- Split Manage All Collections into create, edit, and delete
UPDATE [vault_dev].[dbo].[OrganizationUser]
SET [Permissions] =
JSON_MODIFY(
JSON_MODIFY(
JSON_MODIFY(
[Permissions],
'$.createNewCollections',
CAST(ISNULL(
ISNULL(
JSON_VALUE([Permissions], '$.createNewCollections'),
JSON_VALUE([Permissions], '$.manageAllCollections')),
0) AS BIT)
),
'$.editAnyCollection',
CAST(ISNULL(
ISNULL(
JSON_VALUE([Permissions], '$.editAnyCollection'),
JSON_VALUE([Permissions], '$.manageAllCollections')),
0) AS BIT)
),
'$.deleteAnyCollection',
CAST(ISNULL(
ISNULL(
JSON_VALUE([Permissions], '$.deleteAnyCollection'),
JSON_VALUE([Permissions], '$.manageAllCollections')),
0) AS BIT)
)
WHERE [Permissions] IS NOT NULL
AND ISJSON([Permissions]) > 0
AND (
JSON_VALUE([Permissions], '$.createNewCollections') IS NULL
OR JSON_VALUE([Permissions], '$.editAnyCollection') IS NULL
OR JSON_VALUE([Permissions], '$.deleteAnyCollection') IS NULL
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
using System;
using Bit.Core.Utilities;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Bit.MySqlMigrations.Migrations
{
public partial class SplitManageCollectionsPermissions : Migration
{
private const string _scriptLocation =
"MySqlMigrations.Scripts.2021-09-21_00_SplitManageCollectionsPermission.sql";
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(CoreHelpers.GetEmbeddedResourceContentsAsync(_scriptLocation));
}
protected override void Down(MigrationBuilder migrationBuilder)
{
throw new Exception("Irreversible migration");
}
}
}

View File

@ -16,4 +16,10 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<None Remove="Scripts\2021-09-21_00_SplitManageCollectionsPermission.sql" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Scripts\2021-09-21_00_SplitManageCollectionsPermission.sql" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,56 @@
-- Split Manage Assigned Collections into edit and delete
UPDATE `bw-vault`.`OrganizationUser`
SET `Permissions` =
JSON_INSERT(
`Permissions`,
'$.editAssignedCollections',
IFNULL(
IFNULL(
JSON_EXTRACT(`Permissions`,'$.editAssignedCollections'),
JSON_EXTRACT(`Permissions`, '$.manageAssignedCollections')),
false),
'$.deleteAssignedCollections',
IFNULL(
IFNULL(
JSON_EXTRACT(`Permissions`, '$.deleteAssignedCollections'),
JSON_EXTRACT(`Permissions`, '$.manageAssignedCollections')),
false)
)
WHERE `Permissions` IS NOT NULL
AND JSON_VALID(`Permissions`) > 0
AND (
JSON_EXTRACT(`Permissions`, '$.editAssignedCollections') IS NULL
OR JSON_EXTRACT(`Permissions`, '$.deleteAssignedCollections') IS NULL
);
-- Split Manage All Collections into create, edit, and delete
UPDATE `bw-vault`.`OrganizationUser`
SET `Permissions` =
JSON_INSERT(
`Permissions`,
'$.createNewCollections',
IFNULL(
IFNULL(
JSON_EXTRACT(`Permissions`, '$.createNewColletions'),
JSON_EXTRACT(`Permissions`, '$.manageAllCollections')),
false),
'$.editAnyCollection',
IFNULL(
IFNULL(
JSON_EXTRACT(`Permissions`, '$.editAnyCollection'),
JSON_EXTRACT(`Permissions`, '$.manageAllCollections')),
false),
'$.deleteAnyCollection',
IFNULL(
IFNULL(
JSON_EXTRACT(`Permissions`, '$.deleteAnyCollection'),
JSON_EXTRACT(`Permissions`, '$.manageAllCollections')),
false)
)
WHERE `Permissions` IS NOT NULL
AND JSON_VALID(`Permissions`) > 0
AND (
JSON_EXTRACT(`Permissions`, '$.createNewCollections') IS NULL
OR JSON_EXTRACT(`Permissions`, '$.editAnyCollection') IS NULL
OR JSON_EXTRACT(`Permissions`, '$.deleteAnyCollection') IS NULL
);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
using System;
using Bit.Core.Utilities;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Bit.PostgresMigrations.Migrations
{
public partial class SplitManageCollectionsPermissions : Migration
{
private const string _scriptLocation =
"PostgresMigration.Scripts.2021-09-21_00_SplitManageCollectionsPermission.psql";
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(CoreHelpers.GetEmbeddedResourceContentsAsync(_scriptLocation));
}
protected override void Down(MigrationBuilder migrationBuilder)
{
throw new Exception("Irreversible migration");
}
}
}

View File

@ -16,4 +16,10 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<None Remove="Scripts\2021-09-21_00_SplitManageCollectionsPermission.psql" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Scripts\2021-09-21_00_SplitManageCollectionsPermission.psql" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,42 @@
CREATE OR REPLACE FUNCTION updatePermissionsJson(permissions jsonb) returns jsonb LANGUAGE plpgsql AS $$
DECLARE manageAllCollections jsonb := COALESCE(jsonb_extract_path(permissions, 'manageAllCollections'), 'false');
DECLARE manageAssignedCollections jsonb := COALESCE(jsonb_extract_path(permissions, 'manageAssignedCollections'), 'false');
DECLARE createNewCollections jsonb := COALESCE(jsonb_extract_path(permissions, 'createNewCollections'), manageAllCollections);
DECLARE editAnyCollection jsonb := COALESCE(jsonb_extract_path(permissions, 'editAnyCollection'), manageAllCollections);
DECLARE deleteAnyCollection jsonb := COALESCE(jsonb_extract_path(permissions, 'deleteAnyCollection'), manageAllCollections);
DECLARE editAssignedCollections jsonb := COALESCE(jsonb_extract_path(permissions, 'editAssignedCollections'), manageAssignedCollections);
DECLARE deleteAssignedCollections jsonb := COALESCE(jsonb_extract_path(permissions, 'deleteAssignedCollections'), manageAssignedCollections);
BEGIN
RETURN
jsonb_set(
jsonb_set(
jsonb_set(
jsonb_set(
jsonb_set(
permissions,
'{createNewCollections}',
createNewCollections
),
'{editAnyCollection}',
editAnyCollection
),
'{deleteAnyCollection}',
deleteAnyCollection
),
'{editAssignedCollections}',
editAssignedCollections
),
'{deleteAssignedCollections}',
deleteAssignedCollections
);
END
$$;
UPDATE public."OrganizationUser"
SET "Permissions" = updatePermissionsJson("Permissions"::jsonb)::text
WHERE "Permissions" IS NOT NULL;
DROP FUNCTION updatePermissionsJson(jsonb);