1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-06 18:42:49 -05:00

Merge branch 'main' into jmccannon/ac/pm-16811-scim-invite-optimization

# Conflicts:
#	src/Core/AdminConsole/Services/Implementations/OrganizationService.cs
#	test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs
This commit is contained in:
jrmccannon
2025-03-05 15:24:13 -06:00
272 changed files with 16059 additions and 1921 deletions

View File

@ -306,4 +306,81 @@ public class OrganizationDomainRepositoryTests
var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organizationDomain.DomainName);
Assert.Null(expectedDomain);
}
[DatabaseTheory, DatabaseData]
public async Task GetVerifiedDomainsByOrganizationIdsAsync_ShouldVerifiedDomainsMatchesOrganizationIds(
IOrganizationRepository organizationRepository,
IOrganizationDomainRepository organizationDomainRepository)
{
// Arrange
var guid1 = Guid.NewGuid();
var guid2 = Guid.NewGuid();
var organization1 = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {guid1}",
BillingEmail = $"test+{guid1}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
});
var organization1Domain1 = new OrganizationDomain
{
OrganizationId = organization1.Id,
DomainName = $"domain1+{guid1}@example.com",
Txt = "btw+12345"
};
const int arbitraryNextIteration = 1;
organization1Domain1.SetNextRunDate(arbitraryNextIteration);
organization1Domain1.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organization1Domain1);
var organization1Domain2 = new OrganizationDomain
{
OrganizationId = organization1.Id,
DomainName = $"domain2+{guid1}@example.com",
Txt = "btw+12345"
};
organization1Domain2.SetNextRunDate(arbitraryNextIteration);
await organizationDomainRepository.CreateAsync(organization1Domain2);
var organization2 = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {guid2}",
BillingEmail = $"test+{guid2}@example.com",
Plan = "Test",
PrivateKey = "privatekey",
});
var organization2Domain1 = new OrganizationDomain
{
OrganizationId = organization2.Id,
DomainName = $"domain+{guid2}@example.com",
Txt = "btw+12345"
};
organization2Domain1.SetVerifiedDate();
organization2Domain1.SetNextRunDate(arbitraryNextIteration);
await organizationDomainRepository.CreateAsync(organization2Domain1);
// Act
var domains = await organizationDomainRepository.GetVerifiedDomainsByOrganizationIdsAsync(new[] { organization1.Id });
// Assert
var expectedDomain = domains.FirstOrDefault(domain => domain.DomainName == organization1Domain1.DomainName);
Assert.NotNull(expectedDomain);
var unverifiedDomain = domains.FirstOrDefault(domain => domain.DomainName == organization1Domain2.DomainName);
var otherOrganizationDomain = domains.FirstOrDefault(domain => domain.DomainName == organization2Domain1.DomainName);
Assert.Null(otherOrganizationDomain);
Assert.Null(unverifiedDomain);
}
}

View File

@ -359,6 +359,75 @@ public class OrganizationUserRepositoryTests
Assert.Equal(orgUser1.Id, responseModel.Single().Id);
}
[DatabaseTheory, DatabaseData]
public async Task CreateManyAsync_NoId_Works(IOrganizationRepository organizationRepository,
IUserRepository userRepository,
IOrganizationUserRepository organizationUserRepository)
{
// Arrange
var user1 = await userRepository.CreateTestUserAsync("user1");
var user2 = await userRepository.CreateTestUserAsync("user2");
var user3 = await userRepository.CreateTestUserAsync("user3");
List<User> users = [user1, user2, user3];
var org = await organizationRepository.CreateAsync(new Organization
{
Name = $"test-{Guid.NewGuid()}",
BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULl
});
var orgUsers = users.Select(u => new OrganizationUser
{
OrganizationId = org.Id,
UserId = u.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Owner
});
var createdOrgUserIds = await organizationUserRepository.CreateManyAsync(orgUsers);
var readOrgUsers = await organizationUserRepository.GetManyByOrganizationAsync(org.Id, null);
var readOrgUserIds = readOrgUsers.Select(ou => ou.Id);
Assert.Equal(createdOrgUserIds.ToHashSet(), readOrgUserIds.ToHashSet());
}
[DatabaseTheory, DatabaseData]
public async Task CreateManyAsync_WithId_Works(IOrganizationRepository organizationRepository,
IUserRepository userRepository,
IOrganizationUserRepository organizationUserRepository)
{
// Arrange
var user1 = await userRepository.CreateTestUserAsync("user1");
var user2 = await userRepository.CreateTestUserAsync("user2");
var user3 = await userRepository.CreateTestUserAsync("user3");
List<User> users = [user1, user2, user3];
var org = await organizationRepository.CreateAsync(new Organization
{
Name = $"test-{Guid.NewGuid()}",
BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULl
});
var orgUsers = users.Select(u => new OrganizationUser
{
Id = CoreHelpers.GenerateComb(), // generate ID ahead of time
OrganizationId = org.Id,
UserId = u.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Owner
});
var createdOrgUserIds = await organizationUserRepository.CreateManyAsync(orgUsers);
var readOrgUsers = await organizationUserRepository.GetManyByOrganizationAsync(org.Id, null);
var readOrgUserIds = readOrgUsers.Select(ou => ou.Id);
Assert.Equal(createdOrgUserIds.ToHashSet(), readOrgUserIds.ToHashSet());
}
[DatabaseTheory, DatabaseData]
public async Task CreateManyAsync_WithCollectionAndGroup_SaveSuccessfully(
IOrganizationUserRepository organizationUserRepository,

View File

@ -433,4 +433,454 @@ public class CipherRepositoryTests
Assert.False(unassignedCipherPermission.Read);
Assert.False(unassignedCipherPermission.ViewPassword);
}
[DatabaseTheory, DatabaseData]
public async Task GetCipherPermissionsForOrganizationAsync_ManageProperty_RespectsCollectionUserRules(
ICipherRepository cipherRepository,
IUserRepository userRepository,
ICollectionCipherRepository collectionCipherRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository);
var manageCipher = await CreateCipherInOrganizationCollection(
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
hasManagePermission: true, "Manage Collection");
var nonManageCipher = await CreateCipherInOrganizationCollection(
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
hasManagePermission: false, "Non-Manage Collection");
var permissions = await cipherRepository.GetCipherPermissionsForOrganizationAsync(organization.Id, user.Id);
Assert.Equal(2, permissions.Count);
var managePermission = permissions.FirstOrDefault(c => c.Id == manageCipher.Id);
Assert.NotNull(managePermission);
Assert.True(managePermission.Manage, "Collection with Manage=true should grant Manage permission");
var nonManagePermission = permissions.FirstOrDefault(c => c.Id == nonManageCipher.Id);
Assert.NotNull(nonManagePermission);
Assert.False(nonManagePermission.Manage, "Collection with Manage=false should not grant Manage permission");
}
[DatabaseTheory, DatabaseData]
public async Task GetCipherPermissionsForOrganizationAsync_ManageProperty_RespectsCollectionGroupRules(
ICipherRepository cipherRepository,
IUserRepository userRepository,
ICollectionCipherRepository collectionCipherRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IGroupRepository groupRepository)
{
var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository);
var group = await groupRepository.CreateAsync(new Group
{
OrganizationId = organization.Id,
Name = "Test Group",
});
await groupRepository.UpdateUsersAsync(group.Id, new[] { orgUser.Id });
var (manageCipher, nonManageCipher) = await CreateCipherInOrganizationCollectionWithGroup(
organization, group, cipherRepository, collectionRepository, collectionCipherRepository, groupRepository);
var permissions = await cipherRepository.GetCipherPermissionsForOrganizationAsync(organization.Id, user.Id);
Assert.Equal(2, permissions.Count);
var managePermission = permissions.FirstOrDefault(c => c.Id == manageCipher.Id);
Assert.NotNull(managePermission);
Assert.True(managePermission.Manage, "Collection with Group Manage=true should grant Manage permission");
var nonManagePermission = permissions.FirstOrDefault(c => c.Id == nonManageCipher.Id);
Assert.NotNull(nonManagePermission);
Assert.False(nonManagePermission.Manage, "Collection with Group Manage=false should not grant Manage permission");
}
[DatabaseTheory, DatabaseData]
public async Task GetManyByUserIdAsync_ManageProperty_RespectsCollectionAndOwnershipRules(
ICipherRepository cipherRepository,
IUserRepository userRepository,
ICollectionCipherRepository collectionCipherRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository);
var manageCipher = await CreateCipherInOrganizationCollection(
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
hasManagePermission: true, "Manage Collection");
var nonManageCipher = await CreateCipherInOrganizationCollection(
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
hasManagePermission: false, "Non-Manage Collection");
var personalCipher = await CreatePersonalCipher(user, cipherRepository);
var userCiphers = await cipherRepository.GetManyByUserIdAsync(user.Id);
Assert.Equal(3, userCiphers.Count);
var managePermission = userCiphers.FirstOrDefault(c => c.Id == manageCipher.Id);
Assert.NotNull(managePermission);
Assert.True(managePermission.Manage, "Collection with Manage=true should grant Manage permission");
var nonManagePermission = userCiphers.FirstOrDefault(c => c.Id == nonManageCipher.Id);
Assert.NotNull(nonManagePermission);
Assert.False(nonManagePermission.Manage, "Collection with Manage=false should not grant Manage permission");
var personalPermission = userCiphers.FirstOrDefault(c => c.Id == personalCipher.Id);
Assert.NotNull(personalPermission);
Assert.True(personalPermission.Manage, "Personal ciphers should always have Manage permission");
}
[DatabaseTheory, DatabaseData]
public async Task GetByIdAsync_ManageProperty_RespectsCollectionAndOwnershipRules(
ICipherRepository cipherRepository,
IUserRepository userRepository,
ICollectionCipherRepository collectionCipherRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
var (user, organization, orgUser) = await CreateTestUserAndOrganization(userRepository, organizationRepository, organizationUserRepository);
var manageCipher = await CreateCipherInOrganizationCollection(
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
hasManagePermission: true, "Manage Collection");
var nonManageCipher = await CreateCipherInOrganizationCollection(
organization, orgUser, cipherRepository, collectionRepository, collectionCipherRepository,
hasManagePermission: false, "Non-Manage Collection");
var personalCipher = await CreatePersonalCipher(user, cipherRepository);
var manageDetails = await cipherRepository.GetByIdAsync(manageCipher.Id, user.Id);
Assert.NotNull(manageDetails);
Assert.True(manageDetails.Manage, "Collection with Manage=true should grant Manage permission");
var nonManageDetails = await cipherRepository.GetByIdAsync(nonManageCipher.Id, user.Id);
Assert.NotNull(nonManageDetails);
Assert.False(nonManageDetails.Manage, "Collection with Manage=false should not grant Manage permission");
var personalDetails = await cipherRepository.GetByIdAsync(personalCipher.Id, user.Id);
Assert.NotNull(personalDetails);
Assert.True(personalDetails.Manage, "Personal ciphers should always have Manage permission");
}
private async Task<(User user, Organization org, OrganizationUser orgUser)> CreateTestUserAndOrganization(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
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 Organization",
BillingEmail = user.Email,
Plan = "Test"
});
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
{
UserId = user.Id,
OrganizationId = organization.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Owner,
});
return (user, organization, orgUser);
}
private async Task<Cipher> CreateCipherInOrganizationCollection(
Organization organization,
OrganizationUser orgUser,
ICipherRepository cipherRepository,
ICollectionRepository collectionRepository,
ICollectionCipherRepository collectionCipherRepository,
bool hasManagePermission,
string collectionName)
{
var collection = await collectionRepository.CreateAsync(new Collection
{
Name = collectionName,
OrganizationId = organization.Id,
});
var cipher = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = ""
});
await collectionCipherRepository.UpdateCollectionsForAdminAsync(cipher.Id, organization.Id,
new List<Guid> { collection.Id });
await collectionRepository.UpdateUsersAsync(collection.Id, new List<CollectionAccessSelection>
{
new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = hasManagePermission }
});
return cipher;
}
private async Task<(Cipher manageCipher, Cipher nonManageCipher)> CreateCipherInOrganizationCollectionWithGroup(
Organization organization,
Group group,
ICipherRepository cipherRepository,
ICollectionRepository collectionRepository,
ICollectionCipherRepository collectionCipherRepository,
IGroupRepository groupRepository)
{
var manageCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "Group Manage Collection",
OrganizationId = organization.Id,
});
var nonManageCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "Group Non-Manage Collection",
OrganizationId = organization.Id,
});
var manageCipher = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = ""
});
var nonManageCipher = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = ""
});
await collectionCipherRepository.UpdateCollectionsForAdminAsync(manageCipher.Id, organization.Id,
new List<Guid> { manageCollection.Id });
await collectionCipherRepository.UpdateCollectionsForAdminAsync(nonManageCipher.Id, organization.Id,
new List<Guid> { nonManageCollection.Id });
await groupRepository.ReplaceAsync(group,
new[]
{
new CollectionAccessSelection
{
Id = manageCollection.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
},
new CollectionAccessSelection
{
Id = nonManageCollection.Id,
HidePasswords = false,
ReadOnly = false,
Manage = false
}
});
return (manageCipher, nonManageCipher);
}
private async Task<Cipher> CreatePersonalCipher(User user, ICipherRepository cipherRepository)
{
return await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
UserId = user.Id,
Data = ""
});
}
[DatabaseTheory, DatabaseData]
public async Task GetUserSecurityTasksByCipherIdsAsync_Works(
ICipherRepository cipherRepository,
IUserRepository userRepository,
ICollectionCipherRepository collectionCipherRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IGroupRepository groupRepository
)
{
// Users
var user1 = await userRepository.CreateAsync(new User
{
Name = "Test User 1",
Email = $"test+{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var user2 = await userRepository.CreateAsync(new User
{
Name = "Test User 2",
Email = $"test+{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
// Organization
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Organization",
BillingEmail = user1.Email,
Plan = "Test"
});
// Org Users
var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
{
UserId = user1.Id,
OrganizationId = organization.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Owner,
});
var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser
{
UserId = user2.Id,
OrganizationId = organization.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
});
// A group that will be assigned Edit permissions to any collections
var editGroup = await groupRepository.CreateAsync(new Group
{
OrganizationId = organization.Id,
Name = "Edit Group",
});
await groupRepository.UpdateUsersAsync(editGroup.Id, new[] { orgUser1.Id });
// Add collections to Org
var manageCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "Manage Collection",
OrganizationId = organization.Id
});
// Use a 2nd collection to differentiate between the two users
var manageCollection2 = await collectionRepository.CreateAsync(new Collection
{
Name = "Manage Collection 2",
OrganizationId = organization.Id
});
var viewOnlyCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "View Only Collection",
OrganizationId = organization.Id
});
// Ciphers
var manageCipher1 = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = ""
});
var manageCipher2 = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = ""
});
var viewOnlyCipher = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = ""
});
await collectionCipherRepository.UpdateCollectionsForAdminAsync(manageCipher1.Id, organization.Id,
new List<Guid> { manageCollection.Id });
await collectionCipherRepository.UpdateCollectionsForAdminAsync(manageCipher2.Id, organization.Id,
new List<Guid> { manageCollection2.Id });
await collectionCipherRepository.UpdateCollectionsForAdminAsync(viewOnlyCipher.Id, organization.Id,
new List<Guid> { viewOnlyCollection.Id });
await collectionRepository.UpdateUsersAsync(manageCollection.Id, new List<CollectionAccessSelection>
{
new()
{
Id = orgUser1.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
},
new()
{
Id = orgUser2.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
}
});
// Only add second user to the second manage collection
await collectionRepository.UpdateUsersAsync(manageCollection2.Id, new List<CollectionAccessSelection>
{
new()
{
Id = orgUser2.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
},
});
await collectionRepository.UpdateUsersAsync(viewOnlyCollection.Id, new List<CollectionAccessSelection>
{
new()
{
Id = orgUser1.Id,
HidePasswords = false,
ReadOnly = false,
Manage = false
}
});
var securityTasks = new List<SecurityTask>
{
new SecurityTask { CipherId = manageCipher1.Id, Id = Guid.NewGuid() },
new SecurityTask { CipherId = manageCipher2.Id, Id = Guid.NewGuid() },
new SecurityTask { CipherId = viewOnlyCipher.Id, Id = Guid.NewGuid() }
};
var userSecurityTaskCiphers = await cipherRepository.GetUserSecurityTasksByCipherIdsAsync(organization.Id, securityTasks);
Assert.NotEmpty(userSecurityTaskCiphers);
Assert.Equal(3, userSecurityTaskCiphers.Count);
var user1TaskCiphers = userSecurityTaskCiphers.Where(t => t.UserId == user1.Id);
Assert.Single(user1TaskCiphers);
Assert.Equal(user1.Email, user1TaskCiphers.First().Email);
Assert.Equal(user1.Id, user1TaskCiphers.First().UserId);
Assert.Equal(manageCipher1.Id, user1TaskCiphers.First().CipherId);
var user2TaskCiphers = userSecurityTaskCiphers.Where(t => t.UserId == user2.Id);
Assert.NotNull(user2TaskCiphers);
Assert.Equal(2, user2TaskCiphers.Count());
Assert.Equal(user2.Email, user2TaskCiphers.Last().Email);
Assert.Equal(user2.Id, user2TaskCiphers.Last().UserId);
Assert.Contains(user2TaskCiphers, t => t.CipherId == manageCipher1.Id && t.TaskId == securityTasks[0].Id);
Assert.Contains(user2TaskCiphers, t => t.CipherId == manageCipher2.Id && t.TaskId == securityTasks[1].Id);
}
}