mirror of
https://github.com/bitwarden/server.git
synced 2025-05-25 13:24:50 -05:00
Merge branch 'main' into tools/pm-21917/send-authentication-query
This commit is contained in:
commit
756b2b4e73
@ -151,6 +151,16 @@ public class CiphersController : Controller
|
|||||||
public async Task<CipherResponseModel> Post([FromBody] CipherRequestModel model)
|
public async Task<CipherResponseModel> Post([FromBody] CipherRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
|
|
||||||
|
// Validate the model was encrypted for the posting user
|
||||||
|
if (model.EncryptedFor != null)
|
||||||
|
{
|
||||||
|
if (model.EncryptedFor != user.Id)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var cipher = model.ToCipherDetails(user.Id);
|
var cipher = model.ToCipherDetails(user.Id);
|
||||||
if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
|
if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
|
||||||
{
|
{
|
||||||
@ -170,6 +180,16 @@ public class CiphersController : Controller
|
|||||||
public async Task<CipherResponseModel> PostCreate([FromBody] CipherCreateRequestModel model)
|
public async Task<CipherResponseModel> PostCreate([FromBody] CipherCreateRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
|
|
||||||
|
// Validate the model was encrypted for the posting user
|
||||||
|
if (model.Cipher.EncryptedFor != null)
|
||||||
|
{
|
||||||
|
if (model.Cipher.EncryptedFor != user.Id)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var cipher = model.Cipher.ToCipherDetails(user.Id);
|
var cipher = model.Cipher.ToCipherDetails(user.Id);
|
||||||
if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
|
if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
|
||||||
{
|
{
|
||||||
@ -192,6 +212,16 @@ public class CiphersController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
|
|
||||||
|
// Validate the model was encrypted for the posting user
|
||||||
|
if (model.Cipher.EncryptedFor != null)
|
||||||
|
{
|
||||||
|
if (model.Cipher.EncryptedFor != userId)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await _cipherService.SaveAsync(cipher, userId, model.Cipher.LastKnownRevisionDate, model.CollectionIds, true, false);
|
await _cipherService.SaveAsync(cipher, userId, model.Cipher.LastKnownRevisionDate, model.CollectionIds, true, false);
|
||||||
|
|
||||||
var response = new CipherMiniResponseModel(cipher, _globalSettings, false);
|
var response = new CipherMiniResponseModel(cipher, _globalSettings, false);
|
||||||
@ -209,6 +239,15 @@ public class CiphersController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate the model was encrypted for the posting user
|
||||||
|
if (model.EncryptedFor != null)
|
||||||
|
{
|
||||||
|
if (model.EncryptedFor != user.Id)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ValidateClientVersionForFido2CredentialSupport(cipher);
|
ValidateClientVersionForFido2CredentialSupport(cipher);
|
||||||
|
|
||||||
var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id)).Select(c => c.CollectionId).ToList();
|
var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id)).Select(c => c.CollectionId).ToList();
|
||||||
@ -237,6 +276,15 @@ public class CiphersController : Controller
|
|||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id);
|
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id);
|
||||||
|
|
||||||
|
// Validate the model was encrypted for the posting user
|
||||||
|
if (model.EncryptedFor != null)
|
||||||
|
{
|
||||||
|
if (model.EncryptedFor != userId)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ValidateClientVersionForFido2CredentialSupport(cipher);
|
ValidateClientVersionForFido2CredentialSupport(cipher);
|
||||||
|
|
||||||
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
||||||
@ -658,6 +706,15 @@ public class CiphersController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate the model was encrypted for the posting user
|
||||||
|
if (model.Cipher.EncryptedFor != null)
|
||||||
|
{
|
||||||
|
if (model.Cipher.EncryptedFor != user.Id)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ValidateClientVersionForFido2CredentialSupport(cipher);
|
ValidateClientVersionForFido2CredentialSupport(cipher);
|
||||||
|
|
||||||
var original = cipher.Clone();
|
var original = cipher.Clone();
|
||||||
@ -1019,6 +1076,18 @@ public class CiphersController : Controller
|
|||||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false);
|
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false);
|
||||||
var ciphersDict = ciphers.ToDictionary(c => c.Id);
|
var ciphersDict = ciphers.ToDictionary(c => c.Id);
|
||||||
|
|
||||||
|
// Validate the model was encrypted for the posting user
|
||||||
|
foreach (var cipher in model.Ciphers)
|
||||||
|
{
|
||||||
|
if (cipher.EncryptedFor != null)
|
||||||
|
{
|
||||||
|
if (cipher.EncryptedFor != userId)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var shareCiphers = new List<(Cipher, DateTime?)>();
|
var shareCiphers = new List<(Cipher, DateTime?)>();
|
||||||
foreach (var cipher in model.Ciphers)
|
foreach (var cipher in model.Ciphers)
|
||||||
{
|
{
|
||||||
|
@ -11,6 +11,10 @@ namespace Bit.Api.Vault.Models.Request;
|
|||||||
|
|
||||||
public class CipherRequestModel
|
public class CipherRequestModel
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The Id of the user that encrypted the cipher. It should always represent a UserId.
|
||||||
|
/// </summary>
|
||||||
|
public Guid? EncryptedFor { get; set; }
|
||||||
public CipherType Type { get; set; }
|
public CipherType Type { get; set; }
|
||||||
|
|
||||||
[StringLength(36)]
|
[StringLength(36)]
|
||||||
|
@ -36,6 +36,11 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
|||||||
public string? TwoFactorRecoveryCode { get; set; }
|
public string? TwoFactorRecoveryCode { get; set; }
|
||||||
public string? EquivalentDomains { get; set; }
|
public string? EquivalentDomains { get; set; }
|
||||||
public string? ExcludedGlobalEquivalentDomains { get; set; }
|
public string? ExcludedGlobalEquivalentDomains { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The Account Revision Date is used to check if new sync needs to occur. It should be updated
|
||||||
|
/// whenever a change is made that affects a client's sync data; for example, updating their vault or
|
||||||
|
/// organization membership.
|
||||||
|
/// </summary>
|
||||||
public DateTime AccountRevisionDate { get; set; } = DateTime.UtcNow;
|
public DateTime AccountRevisionDate { get; set; } = DateTime.UtcNow;
|
||||||
public string? Key { get; set; }
|
public string? Key { get; set; }
|
||||||
public string? PublicKey { get; set; }
|
public string? PublicKey { get; set; }
|
||||||
|
@ -115,7 +115,7 @@ public class ImportCiphersCommand : IImportCiphersCommand
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create it all
|
// Create it all
|
||||||
await _cipherRepository.CreateAsync(ciphers, newFolders);
|
await _cipherRepository.CreateAsync(importingUserId, ciphers, newFolders);
|
||||||
|
|
||||||
// push
|
// push
|
||||||
await _pushService.PushSyncVaultAsync(importingUserId);
|
await _pushService.PushSyncVaultAsync(importingUserId);
|
||||||
|
@ -32,7 +32,10 @@ public interface ICipherRepository : IRepository<Cipher, Guid>
|
|||||||
Task DeleteByUserIdAsync(Guid userId);
|
Task DeleteByUserIdAsync(Guid userId);
|
||||||
Task DeleteByOrganizationIdAsync(Guid organizationId);
|
Task DeleteByOrganizationIdAsync(Guid organizationId);
|
||||||
Task UpdateCiphersAsync(Guid userId, IEnumerable<Cipher> ciphers);
|
Task UpdateCiphersAsync(Guid userId, IEnumerable<Cipher> ciphers);
|
||||||
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
|
/// <summary>
|
||||||
|
/// Create ciphers and folders for the specified UserId. Must not be used to create organization owned items.
|
||||||
|
/// </summary>
|
||||||
|
Task CreateAsync(Guid userId, IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
|
||||||
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
|
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
|
||||||
IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers);
|
IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers);
|
||||||
Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId);
|
Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId);
|
||||||
|
@ -484,7 +484,7 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders)
|
public async Task CreateAsync(Guid userId, IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders)
|
||||||
{
|
{
|
||||||
if (!ciphers.Any())
|
if (!ciphers.Any())
|
||||||
{
|
{
|
||||||
@ -518,7 +518,7 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
|||||||
|
|
||||||
await connection.ExecuteAsync(
|
await connection.ExecuteAsync(
|
||||||
$"[{Schema}].[User_BumpAccountRevisionDate]",
|
$"[{Schema}].[User_BumpAccountRevisionDate]",
|
||||||
new { Id = ciphers.First().UserId },
|
new { Id = userId },
|
||||||
commandType: CommandType.StoredProcedure, transaction: transaction);
|
commandType: CommandType.StoredProcedure, transaction: transaction);
|
||||||
|
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using AutoMapper;
|
using System.Diagnostics;
|
||||||
|
using AutoMapper;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -7,11 +8,12 @@ using Bit.Core.Models.Data;
|
|||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.Models;
|
using Bit.Infrastructure.EntityFramework.Models;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
using Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.EntityFramework.Repositories;
|
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||||
|
|
||||||
public class OrganizationUserRepository : Repository<Core.Entities.OrganizationUser, OrganizationUser, Guid>, IOrganizationUserRepository
|
public class OrganizationUserRepository : Repository<Core.Entities.OrganizationUser, OrganizationUser, Guid>, IOrganizationUserRepository
|
||||||
{
|
{
|
||||||
@ -440,15 +442,23 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async override Task ReplaceAsync(Core.Entities.OrganizationUser organizationUser)
|
public override async Task ReplaceAsync(Core.Entities.OrganizationUser organizationUser)
|
||||||
{
|
{
|
||||||
await base.ReplaceAsync(organizationUser);
|
await base.ReplaceAsync(organizationUser);
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
|
||||||
|
// Only bump account revision dates for confirmed OrgUsers,
|
||||||
|
// as this is the only status that receives sync data from the organization
|
||||||
|
if (organizationUser.Status is not OrganizationUserStatusType.Confirmed)
|
||||||
{
|
{
|
||||||
var dbContext = GetDatabaseContext(scope);
|
return;
|
||||||
await dbContext.UserBumpAccountRevisionDateAsync(organizationUser.UserId.GetValueOrDefault());
|
|
||||||
await dbContext.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Debug.Assert(organizationUser.UserId is not null, "OrganizationUser is confirmed but does not have a UserId.");
|
||||||
|
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
await dbContext.UserBumpAccountRevisionDateAsync(organizationUser.UserId.Value);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ReplaceAsync(Core.Entities.OrganizationUser obj, IEnumerable<CollectionAccessSelection> requestedCollections)
|
public async Task ReplaceAsync(Core.Entities.OrganizationUser obj, IEnumerable<CollectionAccessSelection> requestedCollections)
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using System.Diagnostics;
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -11,8 +13,18 @@ namespace Bit.Infrastructure.EntityFramework.Repositories;
|
|||||||
|
|
||||||
public static class DatabaseContextExtensions
|
public static class DatabaseContextExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Bump the account revision date for the user.
|
||||||
|
/// The caller is responsible for providing a valid UserId (not a null or default Guid) for a user that exists
|
||||||
|
/// in the database.
|
||||||
|
/// </summary>
|
||||||
public static async Task UserBumpAccountRevisionDateAsync(this DatabaseContext context, Guid userId)
|
public static async Task UserBumpAccountRevisionDateAsync(this DatabaseContext context, Guid userId)
|
||||||
{
|
{
|
||||||
|
if (userId == Guid.Empty)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Invalid UserId.");
|
||||||
|
}
|
||||||
|
|
||||||
var user = await context.Users.FindAsync(userId);
|
var user = await context.Users.FindAsync(userId);
|
||||||
Debug.Assert(user is not null, "The user id is expected to be validated as a true-in database user before making this call.");
|
Debug.Assert(user is not null, "The user id is expected to be validated as a true-in database user before making this call.");
|
||||||
user.AccountRevisionDate = DateTime.UtcNow;
|
user.AccountRevisionDate = DateTime.UtcNow;
|
||||||
|
@ -142,8 +142,10 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateAsync(IEnumerable<Core.Vault.Entities.Cipher> ciphers, IEnumerable<Core.Vault.Entities.Folder> folders)
|
public async Task CreateAsync(Guid userId, IEnumerable<Core.Vault.Entities.Cipher> ciphers,
|
||||||
|
IEnumerable<Core.Vault.Entities.Folder> folders)
|
||||||
{
|
{
|
||||||
|
ciphers = ciphers.ToList();
|
||||||
if (!ciphers.Any())
|
if (!ciphers.Any())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@ -156,7 +158,8 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
|
|||||||
await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, folderEntities);
|
await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, folderEntities);
|
||||||
var cipherEntities = Mapper.Map<List<Cipher>>(ciphers);
|
var cipherEntities = Mapper.Map<List<Cipher>>(ciphers);
|
||||||
await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, cipherEntities);
|
await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, cipherEntities);
|
||||||
await dbContext.UserBumpAccountRevisionDateAsync(ciphers.First().UserId.GetValueOrDefault());
|
await dbContext.UserBumpAccountRevisionDateAsync(userId);
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ public class ImportCiphersAsyncCommandTests
|
|||||||
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(ciphers, Arg.Any<List<Folder>>());
|
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(importingUserId, ciphers, Arg.Any<List<Folder>>());
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ public class ImportCiphersAsyncCommandTests
|
|||||||
|
|
||||||
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
||||||
|
|
||||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(ciphers, Arg.Any<List<Folder>>());
|
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(importingUserId, ciphers, Arg.Any<List<Folder>>());
|
||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,4 +28,31 @@ public class StaticStoreTests
|
|||||||
Assert.NotNull(plan);
|
Assert.NotNull(plan);
|
||||||
Assert.Equal(planType, plan.Type);
|
Assert.Equal(planType, plan.Type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StaticStore_GlobalEquivalentDomains_OnlyAsciiAllowed()
|
||||||
|
{
|
||||||
|
// Ref: https://daniel.haxx.se/blog/2025/05/16/detecting-malicious-unicode/
|
||||||
|
// URLs can contain unicode characters that to a computer would point to completely seperate domains but to the
|
||||||
|
// naked eye look completely identical. For example 'g' and 'ց' look incredibly similar but when included in a
|
||||||
|
// URL would lead you somewhere different. There is an opening for an attacker to contribute to Bitwarden with a
|
||||||
|
// url update that could be missed in code review and then if they got a user to that URL Bitwarden could
|
||||||
|
// consider it equivalent with a cipher in the users vault and offer autofill when we should not.
|
||||||
|
// GitHub does now show a warning on non-ascii characters but it could still be missed.
|
||||||
|
// https://github.blog/changelog/2025-05-01-github-now-provides-a-warning-about-hidden-unicode-text/
|
||||||
|
|
||||||
|
// To defend against this:
|
||||||
|
// Loop through all equivalent domains and fail if any contain a non-ascii character
|
||||||
|
// non-ascii character can make a valid URL so it's possible that in the future we have a domain
|
||||||
|
// we want to allow list, that should be done through `continue`ing in the below foreach loop
|
||||||
|
// only if the domain strictly equals (do NOT use InvariantCulture comparison) the one added to our allow list.
|
||||||
|
foreach (var domain in StaticStore.GlobalDomains.SelectMany(p => p.Value))
|
||||||
|
{
|
||||||
|
for (var i = 0; i < domain.Length; i++)
|
||||||
|
{
|
||||||
|
var character = domain[i];
|
||||||
|
Assert.True(char.IsAscii(character), $"Domain: {domain} contains non-ASCII character: '{character}' at index: {i}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ using Bit.Core.Test.AutoFixture.Attributes;
|
|||||||
using Bit.Infrastructure.EFIntegration.Test.AutoFixture;
|
using Bit.Infrastructure.EFIntegration.Test.AutoFixture;
|
||||||
using Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers;
|
using Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers;
|
||||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||||
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using EfRepo = Bit.Infrastructure.EntityFramework.Repositories;
|
using EfRepo = Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
using Organization = Bit.Core.AdminConsole.Entities.Organization;
|
using Organization = Bit.Core.AdminConsole.Entities.Organization;
|
||||||
@ -161,7 +162,7 @@ public class OrganizationRepositoryTests
|
|||||||
|
|
||||||
[CiSkippedTheory, EfOrganizationUserAutoData]
|
[CiSkippedTheory, EfOrganizationUserAutoData]
|
||||||
public async Task SearchUnassignedAsync_Works(OrganizationUser orgUser, User user, Organization org,
|
public async Task SearchUnassignedAsync_Works(OrganizationUser orgUser, User user, Organization org,
|
||||||
List<EfRepo.OrganizationUserRepository> efOrgUserRepos, List<EfRepo.OrganizationRepository> efOrgRepos, List<EfRepo.UserRepository> efUserRepos,
|
List<OrganizationUserRepository> efOrgUserRepos, List<EfRepo.OrganizationRepository> efOrgRepos, List<EfRepo.UserRepository> efUserRepos,
|
||||||
SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo, SqlRepo.UserRepository sqlUserRepo)
|
SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo, SqlRepo.UserRepository sqlUserRepo)
|
||||||
{
|
{
|
||||||
orgUser.Type = OrganizationUserType.Owner;
|
orgUser.Type = OrganizationUserType.Owner;
|
||||||
|
@ -24,7 +24,7 @@ public class OrganizationUserRepositoryTests
|
|||||||
{
|
{
|
||||||
[CiSkippedTheory, EfOrganizationUserAutoData]
|
[CiSkippedTheory, EfOrganizationUserAutoData]
|
||||||
public async Task CreateAsync_Works_DataMatches(OrganizationUser orgUser, User user, Organization org,
|
public async Task CreateAsync_Works_DataMatches(OrganizationUser orgUser, User user, Organization org,
|
||||||
OrganizationUserCompare equalityComparer, List<EfRepo.OrganizationUserRepository> suts,
|
OrganizationUserCompare equalityComparer, List<EfAdminConsoleRepo.OrganizationUserRepository> suts,
|
||||||
List<EfRepo.OrganizationRepository> efOrgRepos, List<EfRepo.UserRepository> efUserRepos,
|
List<EfRepo.OrganizationRepository> efOrgRepos, List<EfRepo.UserRepository> efUserRepos,
|
||||||
SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.UserRepository sqlUserRepo,
|
SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.UserRepository sqlUserRepo,
|
||||||
SqlRepo.OrganizationRepository sqlOrgRepo)
|
SqlRepo.OrganizationRepository sqlOrgRepo)
|
||||||
@ -67,7 +67,7 @@ public class OrganizationUserRepositoryTests
|
|||||||
User user,
|
User user,
|
||||||
Organization org,
|
Organization org,
|
||||||
OrganizationUserCompare equalityComparer,
|
OrganizationUserCompare equalityComparer,
|
||||||
List<EfRepo.OrganizationUserRepository> suts,
|
List<EfAdminConsoleRepo.OrganizationUserRepository> suts,
|
||||||
List<EfRepo.UserRepository> efUserRepos,
|
List<EfRepo.UserRepository> efUserRepos,
|
||||||
List<EfRepo.OrganizationRepository> efOrgRepos,
|
List<EfRepo.OrganizationRepository> efOrgRepos,
|
||||||
SqlRepo.OrganizationUserRepository sqlOrgUserRepo,
|
SqlRepo.OrganizationUserRepository sqlOrgUserRepo,
|
||||||
@ -113,7 +113,7 @@ public class OrganizationUserRepositoryTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[CiSkippedTheory, EfOrganizationUserAutoData]
|
[CiSkippedTheory, EfOrganizationUserAutoData]
|
||||||
public async Task DeleteAsync_Works_DataMatches(OrganizationUser orgUser, User user, Organization org, List<EfRepo.OrganizationUserRepository> suts,
|
public async Task DeleteAsync_Works_DataMatches(OrganizationUser orgUser, User user, Organization org, List<EfAdminConsoleRepo.OrganizationUserRepository> suts,
|
||||||
List<EfRepo.UserRepository> efUserRepos, List<EfRepo.OrganizationRepository> efOrgRepos,
|
List<EfRepo.UserRepository> efUserRepos, List<EfRepo.OrganizationRepository> efOrgRepos,
|
||||||
SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.UserRepository sqlUserRepo,
|
SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.UserRepository sqlUserRepo,
|
||||||
SqlRepo.OrganizationRepository sqlOrgRepo)
|
SqlRepo.OrganizationRepository sqlOrgRepo)
|
||||||
@ -188,7 +188,7 @@ public class OrganizationUserRepositoryTests
|
|||||||
List<EfAdminConsoleRepo.PolicyRepository> efPolicyRepository,
|
List<EfAdminConsoleRepo.PolicyRepository> efPolicyRepository,
|
||||||
List<EfRepo.UserRepository> efUserRepository,
|
List<EfRepo.UserRepository> efUserRepository,
|
||||||
List<EfRepo.OrganizationRepository> efOrganizationRepository,
|
List<EfRepo.OrganizationRepository> efOrganizationRepository,
|
||||||
List<EfRepo.OrganizationUserRepository> suts,
|
List<EfAdminConsoleRepo.OrganizationUserRepository> suts,
|
||||||
List<EfAdminConsoleRepo.ProviderRepository> efProviderRepository,
|
List<EfAdminConsoleRepo.ProviderRepository> efProviderRepository,
|
||||||
List<EfAdminConsoleRepo.ProviderOrganizationRepository> efProviderOrganizationRepository,
|
List<EfAdminConsoleRepo.ProviderOrganizationRepository> efProviderOrganizationRepository,
|
||||||
List<EfAdminConsoleRepo.ProviderUserRepository> efProviderUserRepository,
|
List<EfAdminConsoleRepo.ProviderUserRepository> efProviderUserRepository,
|
||||||
|
@ -7,6 +7,7 @@ using Bit.Core.Entities;
|
|||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||||
using Bit.Core.Test.AutoFixture.UserFixtures;
|
using Bit.Core.Test.AutoFixture.UserFixtures;
|
||||||
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
@ -5,6 +5,7 @@ using Bit.Core.Test.AutoFixture.UserFixtures;
|
|||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Bit.Core.Vault.Models.Data;
|
using Bit.Core.Vault.Models.Data;
|
||||||
using Bit.Infrastructure.EFIntegration.Test.AutoFixture.Relays;
|
using Bit.Infrastructure.EFIntegration.Test.AutoFixture.Relays;
|
||||||
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.Vault.Repositories;
|
using Bit.Infrastructure.EntityFramework.Vault.Repositories;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
@ -9,6 +9,7 @@ using Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
|||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using EfAdminConsoleRepo = Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||||
using EfRepo = Bit.Infrastructure.EntityFramework.Repositories;
|
using EfRepo = Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
using EfVaultRepo = Bit.Infrastructure.EntityFramework.Vault.Repositories;
|
using EfVaultRepo = Bit.Infrastructure.EntityFramework.Vault.Repositories;
|
||||||
using SqlRepo = Bit.Infrastructure.Dapper.Repositories;
|
using SqlRepo = Bit.Infrastructure.Dapper.Repositories;
|
||||||
@ -112,7 +113,7 @@ public class CipherRepositoryTests
|
|||||||
[CiSkippedTheory, EfOrganizationCipherCustomize, BitAutoData]
|
[CiSkippedTheory, EfOrganizationCipherCustomize, BitAutoData]
|
||||||
public async Task CreateAsync_BumpsOrgUserAccountRevisionDates(Cipher cipher, List<User> users,
|
public async Task CreateAsync_BumpsOrgUserAccountRevisionDates(Cipher cipher, List<User> users,
|
||||||
List<OrganizationUser> orgUsers, Collection collection, Organization org, List<EfVaultRepo.CipherRepository> suts, List<EfRepo.UserRepository> efUserRepos, List<EfRepo.OrganizationRepository> efOrgRepos,
|
List<OrganizationUser> orgUsers, Collection collection, Organization org, List<EfVaultRepo.CipherRepository> suts, List<EfRepo.UserRepository> efUserRepos, List<EfRepo.OrganizationRepository> efOrgRepos,
|
||||||
List<EfRepo.OrganizationUserRepository> efOrgUserRepos, List<EfRepo.CollectionRepository> efCollectionRepos)
|
List<EfAdminConsoleRepo.OrganizationUserRepository> efOrgUserRepos, List<EfRepo.CollectionRepository> efCollectionRepos)
|
||||||
{
|
{
|
||||||
var savedCiphers = new List<Cipher>();
|
var savedCiphers = new List<Cipher>();
|
||||||
foreach (var sut in suts)
|
foreach (var sut in suts)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -26,15 +27,23 @@ public static class OrganizationTestHelpers
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an Enterprise organization.
|
||||||
|
/// </summary>
|
||||||
public static Task<Organization> CreateTestOrganizationAsync(this IOrganizationRepository organizationRepository,
|
public static Task<Organization> CreateTestOrganizationAsync(this IOrganizationRepository organizationRepository,
|
||||||
string identifier = "test")
|
string identifier = "test")
|
||||||
=> organizationRepository.CreateAsync(new Organization
|
=> organizationRepository.CreateAsync(new Organization
|
||||||
{
|
{
|
||||||
Name = $"{identifier}-{Guid.NewGuid()}",
|
Name = $"{identifier}-{Guid.NewGuid()}",
|
||||||
BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL
|
BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL
|
||||||
Plan = "Test", // TODO: EF does not enforce this being NOT NULl
|
Plan = "Enterprise (Annually)", // TODO: EF does not enforce this being NOT NULl
|
||||||
|
PlanType = PlanType.EnterpriseAnnually
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a confirmed Owner for the specified organization and user.
|
||||||
|
/// Does not include any cryptographic material.
|
||||||
|
/// </summary>
|
||||||
public static Task<OrganizationUser> CreateTestOrganizationUserAsync(
|
public static Task<OrganizationUser> CreateTestOrganizationUserAsync(
|
||||||
this IOrganizationUserRepository organizationUserRepository,
|
this IOrganizationUserRepository organizationUserRepository,
|
||||||
Organization organization,
|
Organization organization,
|
||||||
@ -47,6 +56,17 @@ public static class OrganizationTestHelpers
|
|||||||
Type = OrganizationUserType.Owner
|
Type = OrganizationUserType.Owner
|
||||||
});
|
});
|
||||||
|
|
||||||
|
public static Task<OrganizationUser> CreateTestOrganizationUserInviteAsync(
|
||||||
|
this IOrganizationUserRepository organizationUserRepository,
|
||||||
|
Organization organization)
|
||||||
|
=> organizationUserRepository.CreateAsync(new OrganizationUser
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
UserId = null, // Invites are not linked to a UserId
|
||||||
|
Status = OrganizationUserStatusType.Invited,
|
||||||
|
Type = OrganizationUserType.Owner
|
||||||
|
});
|
||||||
|
|
||||||
public static Task<Group> CreateTestGroupAsync(
|
public static Task<Group> CreateTestGroupAsync(
|
||||||
this IGroupRepository groupRepository,
|
this IGroupRepository groupRepository,
|
||||||
Organization organization,
|
Organization organization,
|
||||||
@ -54,4 +74,14 @@ public static class OrganizationTestHelpers
|
|||||||
=> groupRepository.CreateAsync(
|
=> groupRepository.CreateAsync(
|
||||||
new Group { OrganizationId = organization.Id, Name = $"{identifier} {Guid.NewGuid()}" }
|
new Group { OrganizationId = organization.Id, Name = $"{identifier} {Guid.NewGuid()}" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public static Task<Collection> CreateTestCollectionAsync(
|
||||||
|
this ICollectionRepository collectionRepository,
|
||||||
|
Organization organization,
|
||||||
|
string identifier = "test")
|
||||||
|
=> collectionRepository.CreateAsync(new Collection
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
Name = $"{identifier} {Guid.NewGuid()}"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,105 @@
|
|||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository;
|
||||||
|
|
||||||
|
public class CollectionRepositoryCreateTests
|
||||||
|
{
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task CreateAsync_WithAccess_Works(
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IGroupRepository groupRepository,
|
||||||
|
ICollectionRepository collectionRepository)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||||
|
|
||||||
|
var user1 = await userRepository.CreateTestUserAsync();
|
||||||
|
var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1);
|
||||||
|
|
||||||
|
var user2 = await userRepository.CreateTestUserAsync();
|
||||||
|
var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user2);
|
||||||
|
|
||||||
|
var group1 = await groupRepository.CreateTestGroupAsync(organization);
|
||||||
|
var group2 = await groupRepository.CreateTestGroupAsync(organization);
|
||||||
|
|
||||||
|
var collection = new Collection
|
||||||
|
{
|
||||||
|
Name = "Test Collection Name",
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await collectionRepository.CreateAsync(collection,
|
||||||
|
[
|
||||||
|
new CollectionAccessSelection { Id = group1.Id, Manage = true, HidePasswords = true, ReadOnly = false, },
|
||||||
|
new CollectionAccessSelection { Id = group2.Id, Manage = false, HidePasswords = false, ReadOnly = true, },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
new CollectionAccessSelection { Id = orgUser1.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||||
|
new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = true, ReadOnly = false },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id);
|
||||||
|
|
||||||
|
Assert.NotNull(actualCollection);
|
||||||
|
Assert.Equal("Test Collection Name", actualCollection.Name);
|
||||||
|
|
||||||
|
var groups = actualAccess.Groups.ToArray();
|
||||||
|
Assert.Equal(2, groups.Length);
|
||||||
|
Assert.Single(groups, g => g.Id == group1.Id && g.Manage && g.HidePasswords && !g.ReadOnly);
|
||||||
|
Assert.Single(groups, g => g.Id == group2.Id && !g.Manage && !g.HidePasswords && g.ReadOnly);
|
||||||
|
|
||||||
|
var users = actualAccess.Users.ToArray();
|
||||||
|
Assert.Equal(2, users.Length);
|
||||||
|
Assert.Single(users, u => u.Id == orgUser1.Id && u.Manage && !u.HidePasswords && u.ReadOnly);
|
||||||
|
Assert.Single(users, u => u.Id == orgUser2.Id && !u.Manage && u.HidePasswords && !u.ReadOnly);
|
||||||
|
|
||||||
|
// Clean up data
|
||||||
|
await userRepository.DeleteAsync(user1);
|
||||||
|
await userRepository.DeleteAsync(user2);
|
||||||
|
await organizationRepository.DeleteAsync(organization);
|
||||||
|
await groupRepository.DeleteManyAsync([group1.Id, group2.Id]);
|
||||||
|
await organizationUserRepository.DeleteManyAsync([orgUser1.Id, orgUser2.Id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Makes sure that the sproc handles empty sets.
|
||||||
|
/// </remarks>
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task CreateAsync_WithNoAccess_Works(
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
ICollectionRepository collectionRepository)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||||
|
|
||||||
|
var collection = new Collection
|
||||||
|
{
|
||||||
|
Name = "Test Collection Name",
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await collectionRepository.CreateAsync(collection, [], []);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id);
|
||||||
|
|
||||||
|
Assert.NotNull(actualCollection);
|
||||||
|
Assert.Equal("Test Collection Name", actualCollection.Name);
|
||||||
|
|
||||||
|
Assert.Empty(actualAccess.Groups);
|
||||||
|
Assert.Empty(actualAccess.Users);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
await organizationRepository.DeleteAsync(organization);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,147 @@
|
|||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository;
|
||||||
|
|
||||||
|
public class CollectionRepositoryReplaceTests
|
||||||
|
{
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task ReplaceAsync_WithAccess_Works(
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IGroupRepository groupRepository,
|
||||||
|
ICollectionRepository collectionRepository)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||||
|
|
||||||
|
var user1 = await userRepository.CreateTestUserAsync();
|
||||||
|
var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1);
|
||||||
|
|
||||||
|
var user2 = await userRepository.CreateTestUserAsync();
|
||||||
|
var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user2);
|
||||||
|
|
||||||
|
var user3 = await userRepository.CreateTestUserAsync();
|
||||||
|
var orgUser3 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user3);
|
||||||
|
|
||||||
|
var group1 = await groupRepository.CreateTestGroupAsync(organization);
|
||||||
|
var group2 = await groupRepository.CreateTestGroupAsync(organization);
|
||||||
|
var group3 = await groupRepository.CreateTestGroupAsync(organization);
|
||||||
|
|
||||||
|
var collection = new Collection
|
||||||
|
{
|
||||||
|
Name = "Test Collection Name",
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
};
|
||||||
|
|
||||||
|
await collectionRepository.CreateAsync(collection,
|
||||||
|
[
|
||||||
|
new CollectionAccessSelection { Id = group1.Id, Manage = true, HidePasswords = true, ReadOnly = false, },
|
||||||
|
new CollectionAccessSelection { Id = group2.Id, Manage = false, HidePasswords = false, ReadOnly = true, },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
new CollectionAccessSelection { Id = orgUser1.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||||
|
new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = true, ReadOnly = false },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
collection.Name = "Updated Collection Name";
|
||||||
|
|
||||||
|
await collectionRepository.ReplaceAsync(collection,
|
||||||
|
[
|
||||||
|
// Delete group1
|
||||||
|
// Update group2:
|
||||||
|
new CollectionAccessSelection { Id = group2.Id, Manage = true, HidePasswords = true, ReadOnly = false, },
|
||||||
|
// Add group3:
|
||||||
|
new CollectionAccessSelection { Id = group3.Id, Manage = false, HidePasswords = false, ReadOnly = true, },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// Delete orgUser1
|
||||||
|
// Update orgUser2:
|
||||||
|
new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = false, ReadOnly = true },
|
||||||
|
// Add orgUser3:
|
||||||
|
new CollectionAccessSelection { Id = orgUser3.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id);
|
||||||
|
|
||||||
|
Assert.NotNull(actualCollection);
|
||||||
|
Assert.Equal("Updated Collection Name", actualCollection.Name);
|
||||||
|
|
||||||
|
var groups = actualAccess.Groups.ToArray();
|
||||||
|
Assert.Equal(2, groups.Length);
|
||||||
|
Assert.Single(groups, g => g.Id == group2.Id && g.Manage && g.HidePasswords && !g.ReadOnly);
|
||||||
|
Assert.Single(groups, g => g.Id == group3.Id && !g.Manage && !g.HidePasswords && g.ReadOnly);
|
||||||
|
|
||||||
|
var users = actualAccess.Users.ToArray();
|
||||||
|
|
||||||
|
Assert.Equal(2, users.Length);
|
||||||
|
Assert.Single(users, u => u.Id == orgUser2.Id && !u.Manage && !u.HidePasswords && u.ReadOnly);
|
||||||
|
Assert.Single(users, u => u.Id == orgUser3.Id && u.Manage && !u.HidePasswords && u.ReadOnly);
|
||||||
|
|
||||||
|
// Clean up data
|
||||||
|
await userRepository.DeleteAsync(user1);
|
||||||
|
await userRepository.DeleteAsync(user2);
|
||||||
|
await userRepository.DeleteAsync(user3);
|
||||||
|
await organizationRepository.DeleteAsync(organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Makes sure that the sproc handles empty sets.
|
||||||
|
/// </remarks>
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task ReplaceAsync_WithNoAccess_Works(
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IGroupRepository groupRepository,
|
||||||
|
ICollectionRepository collectionRepository)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||||
|
|
||||||
|
var user = await userRepository.CreateTestUserAsync();
|
||||||
|
var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user);
|
||||||
|
|
||||||
|
var group = await groupRepository.CreateTestGroupAsync(organization);
|
||||||
|
|
||||||
|
var collection = new Collection
|
||||||
|
{
|
||||||
|
Name = "Test Collection Name",
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
};
|
||||||
|
|
||||||
|
await collectionRepository.CreateAsync(collection,
|
||||||
|
[
|
||||||
|
new CollectionAccessSelection { Id = group.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
new CollectionAccessSelection { Id = orgUser.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
collection.Name = "Updated Collection Name";
|
||||||
|
|
||||||
|
await collectionRepository.ReplaceAsync(collection, [], []);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id);
|
||||||
|
|
||||||
|
Assert.NotNull(actualCollection);
|
||||||
|
Assert.Equal("Updated Collection Name", actualCollection.Name);
|
||||||
|
|
||||||
|
Assert.Empty(actualAccess.Groups);
|
||||||
|
Assert.Empty(actualAccess.Users);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
await userRepository.DeleteAsync(user);
|
||||||
|
await organizationRepository.DeleteAsync(organization);
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,7 @@ using Bit.Core.Models.Data;
|
|||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.IntegrationTest.Repositories;
|
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository;
|
||||||
|
|
||||||
public class CollectionRepositoryTests
|
public class CollectionRepositoryTests
|
||||||
{
|
{
|
||||||
@ -463,147 +463,4 @@ public class CollectionRepositoryTests
|
|||||||
Assert.False(c3.Unmanaged);
|
Assert.False(c3.Unmanaged);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[DatabaseTheory, DatabaseData]
|
|
||||||
public async Task ReplaceAsync_Works(
|
|
||||||
IUserRepository userRepository,
|
|
||||||
IOrganizationRepository organizationRepository,
|
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
|
||||||
IGroupRepository groupRepository,
|
|
||||||
ICollectionRepository collectionRepository)
|
|
||||||
{
|
|
||||||
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 orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
|
|
||||||
{
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
UserId = user.Id,
|
|
||||||
Status = OrganizationUserStatusType.Confirmed,
|
|
||||||
});
|
|
||||||
|
|
||||||
var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser
|
|
||||||
{
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
UserId = user.Id,
|
|
||||||
Status = OrganizationUserStatusType.Confirmed,
|
|
||||||
});
|
|
||||||
|
|
||||||
var orgUser3 = await organizationUserRepository.CreateAsync(new OrganizationUser
|
|
||||||
{
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
UserId = user.Id,
|
|
||||||
Status = OrganizationUserStatusType.Confirmed,
|
|
||||||
});
|
|
||||||
|
|
||||||
var group1 = await groupRepository.CreateAsync(new Group
|
|
||||||
{
|
|
||||||
Name = "Test Group #1",
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
});
|
|
||||||
|
|
||||||
var group2 = await groupRepository.CreateAsync(new Group
|
|
||||||
{
|
|
||||||
Name = "Test Group #2",
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
});
|
|
||||||
|
|
||||||
var group3 = await groupRepository.CreateAsync(new Group
|
|
||||||
{
|
|
||||||
Name = "Test Group #3",
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
});
|
|
||||||
|
|
||||||
var collection = new Collection
|
|
||||||
{
|
|
||||||
Name = "Test Collection Name",
|
|
||||||
OrganizationId = organization.Id,
|
|
||||||
};
|
|
||||||
|
|
||||||
await collectionRepository.CreateAsync(collection,
|
|
||||||
[
|
|
||||||
new CollectionAccessSelection { Id = group1.Id, Manage = true, HidePasswords = true, ReadOnly = false, },
|
|
||||||
new CollectionAccessSelection { Id = group2.Id, Manage = false, HidePasswords = false, ReadOnly = true, },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
new CollectionAccessSelection { Id = orgUser1.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
|
||||||
new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = true, ReadOnly = false },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
collection.Name = "Updated Collection Name";
|
|
||||||
|
|
||||||
await collectionRepository.ReplaceAsync(collection,
|
|
||||||
[
|
|
||||||
// Should delete group1
|
|
||||||
new CollectionAccessSelection { Id = group2.Id, Manage = true, HidePasswords = true, ReadOnly = false, },
|
|
||||||
// Should add group3
|
|
||||||
new CollectionAccessSelection { Id = group3.Id, Manage = false, HidePasswords = false, ReadOnly = true, },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
// Should delete orgUser1
|
|
||||||
new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = false, ReadOnly = true },
|
|
||||||
// Should add orgUser3
|
|
||||||
new CollectionAccessSelection { Id = orgUser3.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assert it
|
|
||||||
var info = await collectionRepository.GetByIdWithPermissionsAsync(collection.Id, user.Id, true);
|
|
||||||
|
|
||||||
Assert.NotNull(info);
|
|
||||||
|
|
||||||
Assert.Equal("Updated Collection Name", info.Name);
|
|
||||||
|
|
||||||
var groups = info.Groups.ToArray();
|
|
||||||
|
|
||||||
Assert.Equal(2, groups.Length);
|
|
||||||
|
|
||||||
var actualGroup2 = Assert.Single(groups.Where(g => g.Id == group2.Id));
|
|
||||||
|
|
||||||
Assert.True(actualGroup2.Manage);
|
|
||||||
Assert.True(actualGroup2.HidePasswords);
|
|
||||||
Assert.False(actualGroup2.ReadOnly);
|
|
||||||
|
|
||||||
var actualGroup3 = Assert.Single(groups.Where(g => g.Id == group3.Id));
|
|
||||||
|
|
||||||
Assert.False(actualGroup3.Manage);
|
|
||||||
Assert.False(actualGroup3.HidePasswords);
|
|
||||||
Assert.True(actualGroup3.ReadOnly);
|
|
||||||
|
|
||||||
var users = info.Users.ToArray();
|
|
||||||
|
|
||||||
Assert.Equal(2, users.Length);
|
|
||||||
|
|
||||||
var actualOrgUser2 = Assert.Single(users.Where(u => u.Id == orgUser2.Id));
|
|
||||||
|
|
||||||
Assert.False(actualOrgUser2.Manage);
|
|
||||||
Assert.False(actualOrgUser2.HidePasswords);
|
|
||||||
Assert.True(actualOrgUser2.ReadOnly);
|
|
||||||
|
|
||||||
var actualOrgUser3 = Assert.Single(users.Where(u => u.Id == orgUser3.Id));
|
|
||||||
|
|
||||||
Assert.True(actualOrgUser3.Manage);
|
|
||||||
Assert.False(actualOrgUser3.HidePasswords);
|
|
||||||
Assert.True(actualOrgUser3.ReadOnly);
|
|
||||||
|
|
||||||
// Clean up data
|
|
||||||
await userRepository.DeleteAsync(user);
|
|
||||||
await organizationRepository.DeleteAsync(organization);
|
|
||||||
await groupRepository.DeleteManyAsync([group1.Id, group2.Id, group3.Id]);
|
|
||||||
await organizationUserRepository.DeleteManyAsync([orgUser1.Id, orgUser2.Id, orgUser3.Id]);
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository;
|
||||||
|
|
||||||
|
public class OrganizationUserReplaceTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Specifically tests OrganizationUsers in the invited state, which is unique because
|
||||||
|
/// they're not linked to a UserId.
|
||||||
|
/// </summary>
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task ReplaceAsync_WithCollectionAccess_WhenUserIsInvited_Success(
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
ICollectionRepository collectionRepository)
|
||||||
|
{
|
||||||
|
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||||
|
|
||||||
|
var orgUser = await organizationUserRepository.CreateTestOrganizationUserInviteAsync(organization);
|
||||||
|
|
||||||
|
// Act: update the user, including collection access so we test this overloaded method
|
||||||
|
orgUser.Type = OrganizationUserType.Admin;
|
||||||
|
orgUser.AccessSecretsManager = true;
|
||||||
|
var collection = await collectionRepository.CreateTestCollectionAsync(organization);
|
||||||
|
|
||||||
|
await organizationUserRepository.ReplaceAsync(orgUser, [
|
||||||
|
new CollectionAccessSelection { Id = collection.Id, Manage = true }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var (actualOrgUser, actualCollections) = await organizationUserRepository.GetByIdWithCollectionsAsync(orgUser.Id);
|
||||||
|
Assert.NotNull(actualOrgUser);
|
||||||
|
Assert.Equal(OrganizationUserType.Admin, actualOrgUser.Type);
|
||||||
|
Assert.True(actualOrgUser.AccessSecretsManager);
|
||||||
|
|
||||||
|
var collectionAccess = Assert.Single(actualCollections);
|
||||||
|
Assert.Equal(collection.Id, collectionAccess.Id);
|
||||||
|
Assert.True(collectionAccess.Manage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests OrganizationUsers in the Confirmed status, which is a stand-in for all other
|
||||||
|
/// non-Invited statuses (which are all linked to a UserId).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationRepository"></param>
|
||||||
|
/// <param name="organizationUserRepository"></param>
|
||||||
|
/// <param name="collectionRepository"></param>
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task ReplaceAsync_WithCollectionAccess_WhenUserIsConfirmed_Success(
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
ICollectionRepository collectionRepository)
|
||||||
|
{
|
||||||
|
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||||
|
|
||||||
|
var user = await userRepository.CreateTestUserAsync();
|
||||||
|
// OrganizationUser is linked with the User in the Confirmed status
|
||||||
|
var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user);
|
||||||
|
|
||||||
|
// Act: update the user, including collection access so we test this overloaded method
|
||||||
|
orgUser.Type = OrganizationUserType.Admin;
|
||||||
|
orgUser.AccessSecretsManager = true;
|
||||||
|
var collection = await collectionRepository.CreateTestCollectionAsync(organization);
|
||||||
|
|
||||||
|
await organizationUserRepository.ReplaceAsync(orgUser, [
|
||||||
|
new CollectionAccessSelection { Id = collection.Id, Manage = true }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var (actualOrgUser, actualCollections) = await organizationUserRepository.GetByIdWithCollectionsAsync(orgUser.Id);
|
||||||
|
Assert.NotNull(actualOrgUser);
|
||||||
|
Assert.Equal(OrganizationUserType.Admin, actualOrgUser.Type);
|
||||||
|
Assert.True(actualOrgUser.AccessSecretsManager);
|
||||||
|
|
||||||
|
var collectionAccess = Assert.Single(actualCollections);
|
||||||
|
Assert.Equal(collection.Id, collectionAccess.Id);
|
||||||
|
Assert.True(collectionAccess.Manage);
|
||||||
|
|
||||||
|
// Account revision date should be updated to a later date
|
||||||
|
var actualUser = await userRepository.GetByIdAsync(user.Id);
|
||||||
|
Assert.NotNull(actualUser);
|
||||||
|
Assert.True(actualUser.AccountRevisionDate.CompareTo(user.AccountRevisionDate) > 0);
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,7 @@ using Bit.Core.Repositories;
|
|||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories;
|
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository;
|
||||||
|
|
||||||
public class OrganizationUserRepositoryTests
|
public class OrganizationUserRepositoryTests
|
||||||
{
|
{
|
Loading…
x
Reference in New Issue
Block a user