1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36:14 -05:00

[AC-2084] Include Collection permissions for admin endpoints (#3793)

* [AC-2084] Add documentation to existing collection repository getters

* [AC-2084] Add new CollectionAdminDetails model

* [AC-2084] Add SQL and migration scripts

* [AC-2084] Introduce new repository methods to include permission details for collections

* [AC-2084] Add EF repository methods and integration tests

* [AC-2084] Update CollectionsController and response models

* [AC-2084] Fix failing SqlServer test

* [AC-2084] Clean up admin endpoint response models
- vNext endpoints should now always return CollectionDetailsResponse models
- Update constructors in CollectionDetailsResponseModel to be more explicit and add named static constructors for additional clarity

* [AC-2084] Fix failing tests

* [AC-2084] Fix potential provider/member bug

* [AC-2084] Fix broken collections controller

* [AC-2084] Cleanup collection response model types and constructors

* [AC-2084] Remove redundant authorization check

* [AC-2084] Cleanup ambiguous model name

* [AC-2084] Add GroupBy clause to sprocs

* [AC-2084] Add GroupBy logic to EF repository

* [AC-2084] Update collection repository tests

* [AC-2084] Update migration script date

* Update migration script date

---------

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: kejaeger <138028972+kejaeger@users.noreply.github.com>
This commit is contained in:
Shane Melton
2024-05-03 06:33:06 -07:00
committed by GitHub
parent 25c87214ff
commit d965166a37
14 changed files with 1232 additions and 45 deletions

View File

@ -115,15 +115,20 @@ public class CollectionsControllerTests
await sutProvider.Sut.GetManyWithDetails(organizationAbility.Id);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdWithAccessAsync(userId, organizationAbility.Id, Arg.Any<bool>());
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdWithAccessAsync(organizationAbility.Id);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdWithPermissionsAsync(organizationAbility.Id, userId, true);
}
[Theory, BitAutoData]
public async Task GetOrganizationCollectionsWithGroups_MissingReadAllPermissions_GetsAssignedCollections(
OrganizationAbility organizationAbility, Guid userId, SutProvider<CollectionsController> sutProvider)
OrganizationAbility organizationAbility, Guid userId, SutProvider<CollectionsController> sutProvider, List<CollectionAdminDetails> collections)
{
ArrangeOrganizationAbility(sutProvider, organizationAbility);
collections.ForEach(c => c.OrganizationId = organizationAbility.Id);
collections.ForEach(c => c.Manage = false);
var managedCollection = collections.First();
managedCollection.Manage = true;
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<IAuthorizationService>()
@ -145,10 +150,16 @@ public class CollectionsControllerTests
operation.Name == nameof(BulkCollectionOperations.ReadWithAccess))))
.Returns(AuthorizationResult.Success());
await sutProvider.Sut.GetManyWithDetails(organizationAbility.Id);
sutProvider.GetDependency<ICollectionRepository>()
.GetManyByOrganizationIdWithPermissionsAsync(organizationAbility.Id, userId, true)
.Returns(collections);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdWithAccessAsync(userId, organizationAbility.Id, Arg.Any<bool>());
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceive().GetManyByOrganizationIdWithAccessAsync(organizationAbility.Id);
var response = await sutProvider.Sut.GetManyWithDetails(organizationAbility.Id);
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdWithPermissionsAsync(organizationAbility.Id, userId, true);
Assert.Single(response.Data);
Assert.All(response.Data, c => Assert.Equal(organizationAbility.Id, c.OrganizationId));
Assert.All(response.Data, c => Assert.Equal(managedCollection.Id, c.Id));
}
[Theory, BitAutoData]

View File

@ -36,6 +36,10 @@ public class CollectionCustomization : ICustomization
.With(o => o.OrganizationId, orgId)
.WithGuidFromSeed(cd => cd.Id, _collectionIdSeed));
fixture.Customize<CollectionAdminDetails>(composer => composer
.With(o => o.OrganizationId, orgId)
.WithGuidFromSeed(cd => cd.Id, _collectionIdSeed));
fixture.Customize<CollectionUser>(c => c
.WithGuidFromSeed(cu => cu.OrganizationUserId, _userIdSeed)
.WithGuidFromSeed(cu => cu.CollectionId, _collectionIdSeed));

View File

@ -0,0 +1,459 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Xunit;
namespace Bit.Infrastructure.IntegrationTest.Repositories;
public class CollectionRepositoryTests
{
/// <summary>
/// Test to ensure that access relationships are retrieved when requested
/// </summary>
[DatabaseTheory, DatabaseData]
public async Task GetByIdWithPermissionsAsync_WithRelationships_Success(IUserRepository userRepository,
IOrganizationRepository organizationRepository,
ICollectionRepository collectionRepository,
IOrganizationUserRepository organizationUserRepository, IGroupRepository groupRepository)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
PlanType = PlanType.EnterpriseAnnually,
Plan = "Test Plan",
BillingEmail = "billing@email.com"
});
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
});
var group = await groupRepository.CreateAsync(new Group
{
Name = "Test Group",
OrganizationId = organization.Id,
});
var collection = new Collection { Name = "Test Collection", OrganizationId = organization.Id, };
await collectionRepository.CreateAsync(collection, groups: new[]
{
new CollectionAccessSelection
{
Id = group.Id, HidePasswords = false, ReadOnly = true, Manage = false
}
}, users: new[]
{
new CollectionAccessSelection()
{
Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true
}
});
var collectionWithPermissions = await collectionRepository.GetByIdWithPermissionsAsync(collection.Id, user.Id, true);
Assert.NotNull(collectionWithPermissions);
Assert.Equal(1, collectionWithPermissions.Users?.Count());
Assert.Equal(1, collectionWithPermissions.Groups?.Count());
Assert.True(collectionWithPermissions.Assigned);
Assert.True(collectionWithPermissions.Manage);
Assert.False(collectionWithPermissions.ReadOnly);
Assert.False(collectionWithPermissions.HidePasswords);
}
/// <summary>
/// Test to ensure that a user's explicitly assigned permissions replaces any group permissions
/// that user may belong to
/// </summary>
[DatabaseTheory, DatabaseData]
public async Task GetByIdWithPermissionsAsync_UserOverrideGroup_Success(IUserRepository userRepository,
IOrganizationRepository organizationRepository,
ICollectionRepository collectionRepository,
IOrganizationUserRepository organizationUserRepository, IGroupRepository groupRepository)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
PlanType = PlanType.EnterpriseAnnually,
Plan = "Test Plan",
BillingEmail = "billing@email.com"
});
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
});
var group = await groupRepository.CreateAsync(new Group
{
Name = "Test Group",
OrganizationId = organization.Id,
});
// Assign the test user to the test group
await groupRepository.UpdateUsersAsync(group.Id, new[] { orgUser.Id });
var collection = new Collection { Name = "Test Collection", OrganizationId = organization.Id, };
await collectionRepository.CreateAsync(collection, groups: new[]
{
new CollectionAccessSelection
{
Id = group.Id, HidePasswords = false, ReadOnly = false, Manage = true // Group is Manage
}
}, users: new[]
{
new CollectionAccessSelection()
{
Id = orgUser.Id, HidePasswords = false, ReadOnly = true, Manage = false // User is given ReadOnly (should override group)
}
});
var collectionWithPermissions = await collectionRepository.GetByIdWithPermissionsAsync(collection.Id, user.Id, true);
Assert.NotNull(collectionWithPermissions);
Assert.Equal(1, collectionWithPermissions.Users?.Count());
Assert.Equal(1, collectionWithPermissions.Groups?.Count());
Assert.True(collectionWithPermissions.Assigned);
Assert.False(collectionWithPermissions.Manage);
Assert.True(collectionWithPermissions.ReadOnly);
Assert.False(collectionWithPermissions.HidePasswords);
}
/// <summary>
/// Test to ensure that the returned permissions are the most permissive combination of group permissions when
/// multiple groups are assigned to the same collection with different permissions
/// </summary>
[DatabaseTheory, DatabaseData]
public async Task GetByIdWithPermissionsAsync_CombineGroupPermissions_Success(IUserRepository userRepository,
IOrganizationRepository organizationRepository,
ICollectionRepository collectionRepository,
IOrganizationUserRepository organizationUserRepository, IGroupRepository groupRepository)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
PlanType = PlanType.EnterpriseAnnually,
Plan = "Test Plan",
BillingEmail = "billing@email.com"
});
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
});
var group = await groupRepository.CreateAsync(new Group
{
Name = "Test Group",
OrganizationId = organization.Id,
});
var group2 = await groupRepository.CreateAsync(new Group
{
Name = "Test Group 2",
OrganizationId = organization.Id,
});
// Assign the test user to the test groups
await groupRepository.UpdateUsersAsync(group.Id, new[] { orgUser.Id });
await groupRepository.UpdateUsersAsync(group2.Id, new[] { orgUser.Id });
var collection = new Collection { Name = "Test Collection", OrganizationId = organization.Id, };
await collectionRepository.CreateAsync(collection, groups: new[]
{
new CollectionAccessSelection
{
Id = group.Id, HidePasswords = false, ReadOnly = true, Manage = false // Group 1 is ReadOnly
},
new CollectionAccessSelection
{
Id = group2.Id, HidePasswords = false, ReadOnly = false, Manage = true // Group 2 is Manage
}
}, users: new List<CollectionAccessSelection>()); // No explicit user permissions for this test
var collectionWithPermissions = await collectionRepository.GetByIdWithPermissionsAsync(collection.Id, user.Id, true);
Assert.NotNull(collectionWithPermissions);
Assert.Equal(2, collectionWithPermissions.Groups?.Count());
Assert.True(collectionWithPermissions.Assigned);
// Since Group2 is Manage the user should have Manage
Assert.True(collectionWithPermissions.Manage);
// Similarly, ReadOnly and HidePassword should be false
Assert.False(collectionWithPermissions.ReadOnly);
Assert.False(collectionWithPermissions.HidePasswords);
}
/// <summary>
/// Test to ensure the basic usage works as expected
/// </summary>
[DatabaseTheory, DatabaseData]
public async Task GetManyByOrganizationIdWithPermissionsAsync_Success(IUserRepository userRepository,
IOrganizationRepository organizationRepository,
ICollectionRepository collectionRepository,
IOrganizationUserRepository organizationUserRepository, IGroupRepository groupRepository)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
PlanType = PlanType.EnterpriseAnnually,
Plan = "Test Plan",
BillingEmail = "billing@email.com"
});
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
});
var group = await groupRepository.CreateAsync(new Group
{
Name = "Test Group",
OrganizationId = organization.Id,
});
var collection1 = new Collection { Name = "Collection 1", OrganizationId = organization.Id, };
await collectionRepository.CreateAsync(collection1, groups: new[]
{
new CollectionAccessSelection
{
Id = group.Id, HidePasswords = false, ReadOnly = true, Manage = false
}
}, users: new[]
{
new CollectionAccessSelection()
{
Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true
}
});
var collection2 = new Collection { Name = "Collection 2", OrganizationId = organization.Id, };
await collectionRepository.CreateAsync(collection2, null, users: new[]
{
new CollectionAccessSelection()
{
Id = orgUser.Id, HidePasswords = false, ReadOnly = true, Manage = false
}
});
var collection3 = new Collection { Name = "Collection 3", OrganizationId = organization.Id, };
await collectionRepository.CreateAsync(collection3, groups: new[]
{
new CollectionAccessSelection()
{
Id = group.Id, HidePasswords = false, ReadOnly = false, Manage = true
}
}, null);
var collections = await collectionRepository.GetManyByOrganizationIdWithPermissionsAsync(organization.Id, user.Id, true);
Assert.NotNull(collections);
collections = collections.OrderBy(c => c.Name).ToList();
Assert.Collection(collections, c1 =>
{
Assert.NotNull(c1);
Assert.Equal(1, c1.Users?.Count());
Assert.Equal(1, c1.Groups?.Count());
Assert.True(c1.Assigned);
Assert.True(c1.Manage);
Assert.False(c1.ReadOnly);
Assert.False(c1.HidePasswords);
}, c2 =>
{
Assert.NotNull(c2);
Assert.Equal(1, c2.Users?.Count());
Assert.Equal(0, c2.Groups?.Count());
Assert.True(c2.Assigned);
Assert.False(c2.Manage);
Assert.True(c2.ReadOnly);
Assert.False(c2.HidePasswords);
}, c3 =>
{
Assert.NotNull(c3);
Assert.Equal(0, c3.Users?.Count());
Assert.Equal(1, c3.Groups?.Count());
Assert.False(c3.Assigned);
Assert.False(c3.Manage);
Assert.False(c3.ReadOnly);
Assert.False(c3.HidePasswords);
});
}
/// <summary>
/// Test to ensure collections assigned to multiple groups do not duplicate in the results
/// </summary>
[DatabaseTheory, DatabaseData]
public async Task GetManyByOrganizationIdWithPermissionsAsync_GroupBy_Success(IUserRepository userRepository,
IOrganizationRepository organizationRepository,
ICollectionRepository collectionRepository,
IOrganizationUserRepository organizationUserRepository, IGroupRepository groupRepository)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
PlanType = PlanType.EnterpriseAnnually,
Plan = "Test Plan",
BillingEmail = "billing@email.com"
});
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
});
var group = await groupRepository.CreateAsync(new Group
{
Name = "Test Group",
OrganizationId = organization.Id,
});
var group2 = await groupRepository.CreateAsync(new Group
{
Name = "Test Group 2",
OrganizationId = organization.Id,
});
// Assign the test user to the test groups
await groupRepository.UpdateUsersAsync(group.Id, new[] { orgUser.Id });
await groupRepository.UpdateUsersAsync(group2.Id, new[] { orgUser.Id });
var collection1 = new Collection { Name = "Collection 1", OrganizationId = organization.Id, };
await collectionRepository.CreateAsync(collection1, groups: new[]
{
new CollectionAccessSelection
{
Id = group.Id, HidePasswords = false, ReadOnly = true, Manage = false
},
}, users: new[]
{
new CollectionAccessSelection()
{
Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true
}
});
var collection2 = new Collection { Name = "Collection 2", OrganizationId = organization.Id, };
await collectionRepository.CreateAsync(collection2, null, users: new[]
{
new CollectionAccessSelection()
{
Id = orgUser.Id, HidePasswords = false, ReadOnly = true, Manage = false
}
});
var collection3 = new Collection { Name = "Collection 3", OrganizationId = organization.Id, };
await collectionRepository.CreateAsync(collection3, groups: new[]
{
new CollectionAccessSelection()
{
Id = group.Id, HidePasswords = false, ReadOnly = false, Manage = true
},
new CollectionAccessSelection()
{
Id = group2.Id, HidePasswords = false, ReadOnly = true, Manage = false
}
}, null);
var collections = await collectionRepository.GetManyByOrganizationIdWithPermissionsAsync(organization.Id, user.Id, true);
Assert.NotNull(collections);
Assert.Equal(3, collections.Count);
collections = collections.OrderBy(c => c.Name).ToList();
Assert.Collection(collections, c1 =>
{
Assert.NotNull(c1);
Assert.Equal(1, c1.Users?.Count());
Assert.Equal(1, c1.Groups?.Count());
Assert.True(c1.Assigned);
Assert.True(c1.Manage);
Assert.False(c1.ReadOnly);
Assert.False(c1.HidePasswords);
}, c2 =>
{
Assert.NotNull(c2);
Assert.Equal(1, c2.Users?.Count());
Assert.Equal(0, c2.Groups?.Count());
Assert.True(c2.Assigned);
Assert.False(c2.Manage);
Assert.True(c2.ReadOnly);
Assert.False(c2.HidePasswords);
}, c3 =>
{
Assert.NotNull(c3);
Assert.Equal(0, c3.Users?.Count());
Assert.Equal(2, c3.Groups?.Count());
Assert.True(c3.Assigned); // User is a member of both Groups
Assert.True(c3.Manage); // Group 2 is Manage
Assert.False(c3.ReadOnly);
Assert.False(c3.HidePasswords);
});
}
}