1
0
mirror of https://github.com/bitwarden/server synced 2026-02-21 11:53:42 +00:00
Files
server/test/Infrastructure.IntegrationTest/Repositories/UserRepositoryTests.cs
Maciej Zieniuk 6a7b8f5a89 [PM-31052][PM-32469] Add V2UpgradeToken for key rotation without logout (#6995)
* User V2UpgradeToken for key rotation without logout

* reset old v2 upgrade token on manual key rotation

* sql migration fix

* missing table column

* missing view update

* tests for V2UpgradeToken clearing on manual key rotation

* V2 to V2 rotation causes logout. Updated wrapped key 1 to be a valid V2 encrypted string in tests.

* integration tests failures - increase assert recent for date time type from 2 to 5 seconds (usually for UpdatedAt assertions)

* repository test coverage

* migration script update

* new EF migration scripts

* broken EF migration scripts fixed

* refresh views due to User table alternation
2026-02-20 20:19:14 +01:00

626 lines
25 KiB
C#

using Bit.Core;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.UserKey;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Infrastructure.IntegrationTest.AdminConsole;
using Microsoft.Data.SqlClient;
using Xunit;
namespace Bit.Infrastructure.IntegrationTest.Repositories;
public class UserRepositoryTests
{
[DatabaseTheory, DatabaseData]
public async Task DeleteAsync_Works(IUserRepository userRepository)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
await userRepository.DeleteAsync(user);
var deletedUser = await userRepository.GetByIdAsync(user.Id);
Assert.Null(deletedUser);
}
[Theory, DatabaseData]
public async Task DeleteManyAsync_Works(IUserRepository userRepository, IOrganizationUserRepository organizationUserRepository, IOrganizationRepository organizationRepository, ICollectionRepository collectionRepository, IGroupRepository groupRepository)
{
var user1 = await userRepository.CreateTestUserAsync();
var user2 = await userRepository.CreateTestUserAsync();
var user3 = await userRepository.CreateTestUserAsync();
var organization = await organizationRepository.CreateTestOrganizationAsync();
var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1);
var orgUser3 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user3);
var group1 = await groupRepository.CreateTestGroupAsync(organization, "test-group-1");
var group2 = await groupRepository.CreateTestGroupAsync(organization, "test-group-2");
await groupRepository.UpdateUsersAsync(group1.Id, [orgUser1.Id]);
await groupRepository.UpdateUsersAsync(group2.Id, [orgUser3.Id]);
var collection1 = new Collection
{
OrganizationId = organization.Id,
Name = "test-collection-1"
};
var collection2 = new Collection
{
OrganizationId = organization.Id,
Name = "test-collection-2"
};
await collectionRepository.CreateAsync(
collection1,
groups: [new CollectionAccessSelection { Id = group1.Id, HidePasswords = false, ReadOnly = false, Manage = true }],
users: [new CollectionAccessSelection { Id = orgUser1.Id, HidePasswords = false, ReadOnly = false, Manage = true }]);
await collectionRepository.CreateAsync(collection2,
groups: [new CollectionAccessSelection { Id = group2.Id, HidePasswords = false, ReadOnly = false, Manage = true }],
users: [new CollectionAccessSelection { Id = orgUser3.Id, HidePasswords = false, ReadOnly = false, Manage = true }]);
await userRepository.DeleteManyAsync(new List<User>
{
user1,
user2
});
var deletedUser1 = await userRepository.GetByIdAsync(user1.Id);
var deletedUser2 = await userRepository.GetByIdAsync(user2.Id);
var notDeletedUser3 = await userRepository.GetByIdAsync(user3.Id);
var orgUser1Deleted = await organizationUserRepository.GetByIdAsync(user1.Id);
var notDeletedOrgUsers = await organizationUserRepository.GetManyByUserAsync(user3.Id);
Assert.Null(deletedUser1);
Assert.Null(deletedUser2);
Assert.NotNull(notDeletedUser3);
Assert.Null(orgUser1Deleted);
Assert.NotNull(notDeletedOrgUsers);
Assert.True(notDeletedOrgUsers.Count > 0);
var collection1WithUsers = await collectionRepository.GetByIdWithPermissionsAsync(collection1.Id, null, true);
var collection2WithUsers = await collectionRepository.GetByIdWithPermissionsAsync(collection2.Id, null, true);
Assert.Empty(collection1WithUsers.Users); // Collection1 should have no users (orgUser1 was deleted)
Assert.Single(collection2WithUsers.Users); // Collection2 should still have orgUser3 (not deleted)
Assert.Single(collection2WithUsers.Users);
Assert.Equal(orgUser3.Id, collection2WithUsers.Users.First().Id);
var group1Users = await groupRepository.GetManyUserIdsByIdAsync(group1.Id);
var group2Users = await groupRepository.GetManyUserIdsByIdAsync(group2.Id);
Assert.Empty(group1Users); // Group1 should have no users (orgUser1 was deleted)
Assert.Single(group2Users); // Group2 should still have orgUser3 (not deleted)
Assert.Equal(orgUser3.Id, group2Users.First());
}
[Theory, DatabaseData]
public async Task DeleteAsync_WhenUserHasDefaultUserCollections_MigratesToSharedCollection(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
ICollectionRepository collectionRepository)
{
var user = await userRepository.CreateTestUserAsync();
var organization = await organizationRepository.CreateTestOrganizationAsync();
var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user);
var defaultUserCollection = new Collection
{
Name = "Test Collection",
Type = CollectionType.DefaultUserCollection,
OrganizationId = organization.Id
};
await collectionRepository.CreateAsync(
defaultUserCollection,
groups: null,
users: [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }]);
await userRepository.DeleteAsync(user);
var deletedUser = await userRepository.GetByIdAsync(user.Id);
Assert.Null(deletedUser);
var updatedCollection = await collectionRepository.GetByIdAsync(defaultUserCollection.Id);
Assert.NotNull(updatedCollection);
Assert.Equal(CollectionType.SharedCollection, updatedCollection.Type);
Assert.Equal(user.Email, updatedCollection.DefaultUserCollectionEmail);
}
[Theory, DatabaseData]
public async Task DeleteManyAsync_WhenUsersHaveDefaultUserCollections_MigratesToSharedCollection(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
ICollectionRepository collectionRepository)
{
var user1 = await userRepository.CreateTestUserAsync();
var user2 = await userRepository.CreateTestUserAsync();
var organization = await organizationRepository.CreateTestOrganizationAsync();
var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1);
var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user2);
var defaultUserCollection1 = new Collection
{
Name = "Test Collection 1",
Type = CollectionType.DefaultUserCollection,
OrganizationId = organization.Id
};
var defaultUserCollection2 = new Collection
{
Name = "Test Collection 2",
Type = CollectionType.DefaultUserCollection,
OrganizationId = organization.Id
};
await collectionRepository.CreateAsync(defaultUserCollection1, groups: null, users: [new CollectionAccessSelection { Id = orgUser1.Id, HidePasswords = false, ReadOnly = false, Manage = true }]);
await collectionRepository.CreateAsync(defaultUserCollection2, groups: null, users: [new CollectionAccessSelection { Id = orgUser2.Id, HidePasswords = false, ReadOnly = false, Manage = true }]);
await userRepository.DeleteManyAsync([user1, user2]);
var deletedUser1 = await userRepository.GetByIdAsync(user1.Id);
var deletedUser2 = await userRepository.GetByIdAsync(user2.Id);
Assert.Null(deletedUser1);
Assert.Null(deletedUser2);
var updatedCollection1 = await collectionRepository.GetByIdAsync(defaultUserCollection1.Id);
Assert.NotNull(updatedCollection1);
Assert.Equal(CollectionType.SharedCollection, updatedCollection1.Type);
Assert.Equal(user1.Email, updatedCollection1.DefaultUserCollectionEmail);
var updatedCollection2 = await collectionRepository.GetByIdAsync(defaultUserCollection2.Id);
Assert.NotNull(updatedCollection2);
Assert.Equal(CollectionType.SharedCollection, updatedCollection2.Type);
Assert.Equal(user2.Email, updatedCollection2.DefaultUserCollectionEmail);
}
[Theory, DatabaseData]
public async Task GetPremiumAccessAsync_WithPersonalPremium_ReturnsCorrectAccess(
IUserRepository userRepository)
{
// Arrange
var user = await userRepository.CreateAsync(new User
{
Name = "Premium User",
Email = $"premium+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Premium = true
});
// Act
var result = await userRepository.GetPremiumAccessAsync(user.Id);
// Assert
Assert.NotNull(result);
Assert.True(result.PersonalPremium);
Assert.False(result.OrganizationPremium);
Assert.True(result.HasPremiumAccess);
}
[Theory, DatabaseData]
public async Task GetPremiumAccessAsync_WithOrganizationPremium_ReturnsCorrectAccess(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
// Arrange
var user = await userRepository.CreateAsync(new User
{
Name = "Org User",
Email = $"org+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Premium = false
});
var organization = await organizationRepository.CreateTestOrganizationAsync();
await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user);
// Act
var result = await userRepository.GetPremiumAccessAsync(user.Id);
// Assert
Assert.NotNull(result);
Assert.False(result.PersonalPremium);
Assert.True(result.OrganizationPremium);
Assert.True(result.HasPremiumAccess);
}
[Theory, DatabaseData]
public async Task GetPremiumAccessAsync_WithDisabledOrganization_ReturnsNoOrganizationPremium(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
// Arrange
var user = await userRepository.CreateAsync(new User
{
Name = "User",
Email = $"user+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Premium = false
});
var organization = await organizationRepository.CreateTestOrganizationAsync();
organization.Enabled = false;
await organizationRepository.ReplaceAsync(organization);
await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user);
// Act
var result = await userRepository.GetPremiumAccessAsync(user.Id);
// Assert
Assert.NotNull(result);
Assert.False(result.OrganizationPremium);
Assert.False(result.HasPremiumAccess);
}
[Theory, DatabaseData]
public async Task GetPremiumAccessAsync_WithOrganizationUsersGetPremiumFalse_ReturnsNoOrganizationPremium(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
// Arrange
var user = await userRepository.CreateAsync(new User
{
Name = "User",
Email = $"{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Premium = false
});
var organization = await organizationRepository.CreateTestOrganizationAsync();
organization.UsersGetPremium = false;
await organizationRepository.ReplaceAsync(organization);
await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user);
// Act
var result = await userRepository.GetPremiumAccessAsync(user.Id);
// Assert
Assert.NotNull(result);
Assert.False(result.OrganizationPremium);
Assert.False(result.HasPremiumAccess);
}
[Theory, DatabaseData]
public async Task GetPremiumAccessAsync_WithMultipleOrganizations_OneProvidesPremium_ReturnsOrganizationPremium(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
// Arrange
var user = await userRepository.CreateAsync(new User
{
Name = "User With Premium Org",
Email = $"{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Premium = false
});
var orgWithPremium = await organizationRepository.CreateTestOrganizationAsync();
await organizationUserRepository.CreateTestOrganizationUserAsync(orgWithPremium, user);
var orgNoPremium = await organizationRepository.CreateTestOrganizationAsync();
orgNoPremium.UsersGetPremium = false;
await organizationRepository.ReplaceAsync(orgNoPremium);
await organizationUserRepository.CreateTestOrganizationUserAsync(orgNoPremium, user);
// Act
var result = await userRepository.GetPremiumAccessAsync(user.Id);
// Assert
Assert.NotNull(result);
Assert.False(result.PersonalPremium);
Assert.True(result.OrganizationPremium);
Assert.True(result.HasPremiumAccess);
}
[Theory, DatabaseData]
public async Task GetPremiumAccessAsync_WithMultipleOrganizations_NoneProvidePremium_ReturnsNoOrganizationPremium(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
// Arrange
var user = await userRepository.CreateAsync(new User
{
Name = "User With No Premium Orgs",
Email = $"{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Premium = false
});
var disabledOrg = await organizationRepository.CreateTestOrganizationAsync();
disabledOrg.Enabled = false;
await organizationRepository.ReplaceAsync(disabledOrg);
await organizationUserRepository.CreateTestOrganizationUserAsync(disabledOrg, user);
var orgNoPremium = await organizationRepository.CreateTestOrganizationAsync();
orgNoPremium.UsersGetPremium = false;
await organizationRepository.ReplaceAsync(orgNoPremium);
await organizationUserRepository.CreateTestOrganizationUserAsync(orgNoPremium, user);
// Act
var result = await userRepository.GetPremiumAccessAsync(user.Id);
// Assert
Assert.NotNull(result);
Assert.False(result.PersonalPremium);
Assert.False(result.OrganizationPremium);
Assert.False(result.HasPremiumAccess);
}
[Theory, DatabaseData]
public async Task GetPremiumAccessAsync_NonExistentUser_ReturnsNull(
IUserRepository userRepository)
{
// Act
var result = await userRepository.GetPremiumAccessAsync(Guid.NewGuid());
// Assert
Assert.Null(result);
}
[Theory, DatabaseData]
public async Task GetPremiumAccessByIdsAsync_MultipleUsers_ReturnsCorrectAccessForEach(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
// Arrange
var personalPremiumUser = await userRepository.CreateAsync(new User
{
Name = "Personal Premium",
Email = $"{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Premium = true
});
var orgPremiumUser = await userRepository.CreateAsync(new User
{
Name = "Org Premium",
Email = $"{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Premium = false
});
var bothPremiumUser = await userRepository.CreateAsync(new User
{
Name = "Both Premium",
Email = $"{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Premium = true
});
var noPremiumUser = await userRepository.CreateAsync(new User
{
Name = "No Premium",
Email = $"{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Premium = false
});
var multiOrgUser = await userRepository.CreateAsync(new User
{
Name = "Multi Org User",
Email = $"{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Premium = false
});
var personalPremiumWithDisabledOrg = await userRepository.CreateAsync(new User
{
Name = "Personal Premium With Disabled Org",
Email = $"{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Premium = true
});
var organization = await organizationRepository.CreateTestOrganizationAsync();
await organizationUserRepository.CreateTestOrganizationUserAsync(organization, orgPremiumUser);
await organizationUserRepository.CreateTestOrganizationUserAsync(organization, bothPremiumUser);
await organizationUserRepository.CreateTestOrganizationUserAsync(organization, multiOrgUser);
var orgWithoutPremium = await organizationRepository.CreateTestOrganizationAsync();
orgWithoutPremium.UsersGetPremium = false;
await organizationRepository.ReplaceAsync(orgWithoutPremium);
await organizationUserRepository.CreateTestOrganizationUserAsync(orgWithoutPremium, multiOrgUser);
var disabledOrg = await organizationRepository.CreateTestOrganizationAsync();
disabledOrg.Enabled = false;
await organizationRepository.ReplaceAsync(disabledOrg);
await organizationUserRepository.CreateTestOrganizationUserAsync(disabledOrg, personalPremiumWithDisabledOrg);
// Act
var results = await userRepository.GetPremiumAccessByIdsAsync([
personalPremiumUser.Id,
orgPremiumUser.Id,
bothPremiumUser.Id,
noPremiumUser.Id,
multiOrgUser.Id,
personalPremiumWithDisabledOrg.Id
]);
var resultsList = results.ToList();
// Assert
Assert.Equal(6, resultsList.Count);
var personalResult = resultsList.First(r => r.Id == personalPremiumUser.Id);
Assert.True(personalResult.PersonalPremium);
Assert.False(personalResult.OrganizationPremium);
var orgResult = resultsList.First(r => r.Id == orgPremiumUser.Id);
Assert.False(orgResult.PersonalPremium);
Assert.True(orgResult.OrganizationPremium);
var bothResult = resultsList.First(r => r.Id == bothPremiumUser.Id);
Assert.True(bothResult.PersonalPremium);
Assert.True(bothResult.OrganizationPremium);
var noneResult = resultsList.First(r => r.Id == noPremiumUser.Id);
Assert.False(noneResult.PersonalPremium);
Assert.False(noneResult.OrganizationPremium);
var multiResult = resultsList.First(r => r.Id == multiOrgUser.Id);
Assert.False(multiResult.PersonalPremium);
Assert.True(multiResult.OrganizationPremium);
var personalWithDisabledOrgResult = resultsList.First(r => r.Id == personalPremiumWithDisabledOrg.Id);
Assert.True(personalWithDisabledOrgResult.PersonalPremium);
Assert.False(personalWithDisabledOrgResult.OrganizationPremium);
}
[Theory, DatabaseData]
public async Task GetPremiumAccessByIdsAsync_EmptyList_ReturnsEmptyResult(
IUserRepository userRepository)
{
// Act
var results = await userRepository.GetPremiumAccessByIdsAsync([]);
// Assert
Assert.Empty(results);
}
[Theory, DatabaseData]
public async Task SetKeyConnectorUserKey_UpdatesUserKey(IUserRepository userRepository, Database database)
{
var user = await userRepository.CreateTestUserAsync();
const string keyConnectorWrappedKey = "key-connector-wrapped-user-key";
var setKeyConnectorUserKeyDelegate = userRepository.SetKeyConnectorUserKey(user.Id, keyConnectorWrappedKey);
await RunUpdateUserDataAsync(setKeyConnectorUserKeyDelegate, database);
var updatedUser = await userRepository.GetByIdAsync(user.Id);
Assert.NotNull(updatedUser);
Assert.Equal(keyConnectorWrappedKey, updatedUser.Key);
Assert.True(updatedUser.UsesKeyConnector);
Assert.Equal(KdfType.Argon2id, updatedUser.Kdf);
Assert.Equal(AuthConstants.ARGON2_ITERATIONS.Default, updatedUser.KdfIterations);
Assert.Equal(AuthConstants.ARGON2_MEMORY.Default, updatedUser.KdfMemory);
Assert.Equal(AuthConstants.ARGON2_PARALLELISM.Default, updatedUser.KdfParallelism);
Assert.Equal(DateTime.UtcNow, updatedUser.RevisionDate, TimeSpan.FromMinutes(1));
Assert.Equal(DateTime.UtcNow, updatedUser.AccountRevisionDate, TimeSpan.FromMinutes(1));
}
[Theory, DatabaseData]
public async Task UpdateUserKeyAndEncryptedDataV2Async_UpdatesAllUserFields(IUserRepository userRepository)
{
// Arrange
var user = await userRepository.CreateTestUserAsync();
var newSecurityStamp = Guid.NewGuid().ToString();
user.Key = "new-user-key";
user.PrivateKey = "new-private-key";
user.SecurityStamp = newSecurityStamp;
user.Kdf = KdfType.Argon2id;
user.KdfIterations = 3;
user.KdfMemory = 64;
user.KdfParallelism = 4;
user.Email = $"updated+{Guid.NewGuid()}@example.com";
user.MasterPassword = "new-master-password-hash";
user.MasterPasswordHint = "new-hint";
user.LastKeyRotationDate = DateTime.UtcNow;
user.RevisionDate = DateTime.UtcNow;
user.AccountRevisionDate = DateTime.UtcNow;
user.SignedPublicKey = "new-signed-public-key";
user.SecurityState = "new-security-state";
user.SecurityVersion = 2;
user.V2UpgradeToken = null;
// Act
await userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, []);
// Assert
var updatedUser = await userRepository.GetByIdAsync(user.Id);
Assert.NotNull(updatedUser);
Assert.Equal("new-user-key", updatedUser.Key);
Assert.Equal("new-private-key", updatedUser.PrivateKey);
Assert.Equal(newSecurityStamp, updatedUser.SecurityStamp);
Assert.Equal(KdfType.Argon2id, updatedUser.Kdf);
Assert.Equal(3, updatedUser.KdfIterations);
Assert.Equal(64, updatedUser.KdfMemory);
Assert.Equal(4, updatedUser.KdfParallelism);
Assert.Equal(user.Email, updatedUser.Email);
Assert.Equal("new-master-password-hash", updatedUser.MasterPassword);
Assert.Equal("new-hint", updatedUser.MasterPasswordHint);
Assert.Equal("new-signed-public-key", updatedUser.SignedPublicKey);
Assert.Equal("new-security-state", updatedUser.SecurityState);
Assert.Equal(2, updatedUser.SecurityVersion);
Assert.Null(updatedUser.V2UpgradeToken);
Assert.Equal(DateTime.UtcNow, updatedUser.RevisionDate, TimeSpan.FromMinutes(1));
}
[Theory, DatabaseData]
public async Task UpdateUserKeyAndEncryptedDataV2Async_InvokesUpdateDataActions(IUserRepository userRepository)
{
// Arrange
var user = await userRepository.CreateTestUserAsync();
user.RevisionDate = DateTime.UtcNow;
var actionWasInvoked = false;
UpdateEncryptedDataForKeyRotation action = (_, _) =>
{
actionWasInvoked = true;
return Task.CompletedTask;
};
// Act
await userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, [action]);
// Assert
Assert.True(actionWasInvoked);
}
private static async Task RunUpdateUserDataAsync(UpdateUserData task, Database database)
{
if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf)
{
await using var connection = new SqlConnection(database.ConnectionString);
connection.Open();
await using var transaction = connection.BeginTransaction();
try
{
await task(connection, transaction);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
else
{
await task();
}
}
}