1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05:00

[PM-14378] SecurityTask Authorization Handler (#5039)

* [PM-14378] Introduce GetCipherPermissionsForOrganization query for Dapper CipherRepository

* [PM-14378] Introduce GetCipherPermissionsForOrganization method for Entity Framework

* [PM-14378] Add integration tests for new repository method

* [PM-14378] Introduce IGetCipherPermissionsForUserQuery CQRS query

* [PM-14378] Introduce SecurityTaskOperationRequirement

* [PM-14378] Introduce SecurityTaskAuthorizationHandler.cs

* [PM-14378] Introduce SecurityTaskOrganizationAuthorizationHandler.cs

* [PM-14378] Register new authorization handlers

* [PM-14378] Formatting

* [PM-14378] Add unit tests for GetCipherPermissionsForUserQuery

* [PM-15378] Cleanup SecurityTaskAuthorizationHandler and add tests

* [PM-14378] Add tests for SecurityTaskOrganizationAuthorizationHandler

* [PM-14378] Formatting

* [PM-14378] Update date in migration file

* [PM-14378] Add missing awaits

* [PM-14378] Bump migration script date

* [PM-14378] Remove Unassigned property from OrganizationCipherPermission as it was making the query too complicated

* [PM-14378] Update sproc to use Union All to improve query performance

* [PM-14378] Bump migration script date
This commit is contained in:
Shane Melton
2025-01-09 12:14:24 -08:00
committed by GitHub
parent fd195e7cf3
commit a99f82dddd
18 changed files with 1669 additions and 0 deletions

View File

@ -0,0 +1,430 @@
using System.Security.Claims;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Core.Vault.Authorization.SecurityTasks;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Queries;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Vault.Authorization;
[SutProviderCustomize]
public class SecurityTaskAuthorizationHandlerTests
{
[Theory, CurrentContextOrganizationCustomize, BitAutoData]
public async Task MissingOrg_Failure(
CurrentContextOrganization organization,
SutProvider<SecurityTaskAuthorizationHandler> sutProvider)
{
var userId = Guid.NewGuid();
var task = new SecurityTask
{
OrganizationId = organization.Id,
CipherId = Guid.NewGuid()
};
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns((CurrentContextOrganization)null);
var context = new AuthorizationHandlerContext(
new[] { SecurityTaskOperations.Read },
new ClaimsPrincipal(),
task);
await sutProvider.Sut.HandleAsync(context);
Assert.False(context.HasSucceeded);
}
[Theory, CurrentContextOrganizationCustomize, BitAutoData]
public async Task MissingCipherId_Failure(
CurrentContextOrganization organization,
SutProvider<SecurityTaskAuthorizationHandler> sutProvider)
{
var operations = new[]
{
SecurityTaskOperations.Read, SecurityTaskOperations.Create, SecurityTaskOperations.Update
};
var userId = Guid.NewGuid();
var task = new SecurityTask
{
OrganizationId = organization.Id,
CipherId = null
};
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
foreach (var operation in operations)
{
var context = new AuthorizationHandlerContext(
new[] { operation },
new ClaimsPrincipal(),
task);
await sutProvider.Sut.HandleAsync(context);
Assert.False(context.HasSucceeded, operation.ToString());
}
}
[Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.User), BitAutoData]
public async Task Read_User_CanReadCipher_Success(
CurrentContextOrganization organization,
SutProvider<SecurityTaskAuthorizationHandler> sutProvider)
{
var userId = Guid.NewGuid();
var task = new SecurityTask
{
OrganizationId = organization.Id,
CipherId = Guid.NewGuid()
};
var cipherPermissions = new OrganizationCipherPermission
{
Id = task.CipherId.Value,
Read = true
};
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IGetCipherPermissionsForUserQuery>().GetByOrganization(organization.Id).Returns(new Dictionary<Guid, OrganizationCipherPermission>
{
{ task.CipherId.Value, cipherPermissions }
});
var context = new AuthorizationHandlerContext(
new[] { SecurityTaskOperations.Read },
new ClaimsPrincipal(),
task);
await sutProvider.Sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
[Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin), BitAutoData]
public async Task Read_Admin_Success(
CurrentContextOrganization organization,
SutProvider<SecurityTaskAuthorizationHandler> sutProvider)
{
var userId = Guid.NewGuid();
var task = new SecurityTask
{
OrganizationId = organization.Id,
CipherId = Guid.NewGuid()
};
var cipherPermissions = new OrganizationCipherPermission
{
Id = task.CipherId.Value,
};
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IGetCipherPermissionsForUserQuery>().GetByOrganization(organization.Id).Returns(new Dictionary<Guid, OrganizationCipherPermission>
{
{ task.CipherId.Value, cipherPermissions }
});
var context = new AuthorizationHandlerContext(
new[] { SecurityTaskOperations.Read },
new ClaimsPrincipal(),
task);
await sutProvider.Sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
[Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin), BitAutoData]
public async Task Read_Admin_MissingCipher_Failure(
CurrentContextOrganization organization,
SutProvider<SecurityTaskAuthorizationHandler> sutProvider)
{
var userId = Guid.NewGuid();
var task = new SecurityTask
{
OrganizationId = organization.Id,
CipherId = Guid.NewGuid()
};
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IGetCipherPermissionsForUserQuery>().GetByOrganization(organization.Id).Returns(new Dictionary<Guid, OrganizationCipherPermission>());
var context = new AuthorizationHandlerContext(
new[] { SecurityTaskOperations.Read },
new ClaimsPrincipal(),
task);
await sutProvider.Sut.HandleAsync(context);
Assert.False(context.HasSucceeded);
}
[Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.User), BitAutoData]
public async Task Read_User_CannotReadCipher_Failure(
CurrentContextOrganization organization,
SutProvider<SecurityTaskAuthorizationHandler> sutProvider)
{
var userId = Guid.NewGuid();
var task = new SecurityTask
{
OrganizationId = organization.Id,
CipherId = Guid.NewGuid()
};
var cipherPermissions = new OrganizationCipherPermission
{
Id = task.CipherId.Value,
Read = false
};
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IGetCipherPermissionsForUserQuery>().GetByOrganization(organization.Id).Returns(new Dictionary<Guid, OrganizationCipherPermission>
{
{ task.CipherId.Value, cipherPermissions }
});
var context = new AuthorizationHandlerContext(
new[] { SecurityTaskOperations.Read },
new ClaimsPrincipal(),
task);
await sutProvider.Sut.HandleAsync(context);
Assert.False(context.HasSucceeded);
}
[Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.User), BitAutoData]
public async Task Create_User_Failure(
CurrentContextOrganization organization,
SutProvider<SecurityTaskAuthorizationHandler> sutProvider)
{
var userId = Guid.NewGuid();
var task = new SecurityTask
{
OrganizationId = organization.Id,
CipherId = Guid.NewGuid()
};
var cipherPermissions = new OrganizationCipherPermission
{
Id = task.CipherId.Value,
Read = true,
Edit = true,
};
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IGetCipherPermissionsForUserQuery>().GetByOrganization(organization.Id).Returns(new Dictionary<Guid, OrganizationCipherPermission>
{
{ task.CipherId.Value, cipherPermissions }
});
var context = new AuthorizationHandlerContext(
new[] { SecurityTaskOperations.Create },
new ClaimsPrincipal(),
task);
await sutProvider.Sut.HandleAsync(context);
Assert.False(context.HasSucceeded);
}
[Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin), BitAutoData]
public async Task Create_Admin_MissingCipher_Failure(
CurrentContextOrganization organization,
SutProvider<SecurityTaskAuthorizationHandler> sutProvider)
{
var userId = Guid.NewGuid();
var task = new SecurityTask
{
OrganizationId = organization.Id,
CipherId = Guid.NewGuid()
};
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IGetCipherPermissionsForUserQuery>().GetByOrganization(organization.Id).Returns(new Dictionary<Guid, OrganizationCipherPermission>());
var context = new AuthorizationHandlerContext(
new[] { SecurityTaskOperations.Create },
new ClaimsPrincipal(),
task);
await sutProvider.Sut.HandleAsync(context);
Assert.False(context.HasSucceeded);
}
[Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin), BitAutoData]
public async Task Create_Admin_Success(
CurrentContextOrganization organization,
SutProvider<SecurityTaskAuthorizationHandler> sutProvider)
{
var userId = Guid.NewGuid();
var task = new SecurityTask
{
OrganizationId = organization.Id,
CipherId = Guid.NewGuid()
};
var cipherPermissions = new OrganizationCipherPermission
{
Id = task.CipherId.Value,
Read = true,
Edit = true,
};
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IGetCipherPermissionsForUserQuery>().GetByOrganization(organization.Id).Returns(new Dictionary<Guid, OrganizationCipherPermission>
{
{ task.CipherId.Value, cipherPermissions }
});
var context = new AuthorizationHandlerContext(
new[] { SecurityTaskOperations.Create },
new ClaimsPrincipal(),
task);
await sutProvider.Sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
[Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.User), BitAutoData]
public async Task Update_User_CanEditCipher_Success(
CurrentContextOrganization organization,
SutProvider<SecurityTaskAuthorizationHandler> sutProvider)
{
var userId = Guid.NewGuid();
var task = new SecurityTask
{
OrganizationId = organization.Id,
CipherId = Guid.NewGuid()
};
var cipherPermissions = new OrganizationCipherPermission
{
Id = task.CipherId.Value,
Read = true,
Edit = true
};
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IGetCipherPermissionsForUserQuery>().GetByOrganization(organization.Id).Returns(new Dictionary<Guid, OrganizationCipherPermission>
{
{ task.CipherId.Value, cipherPermissions }
});
var context = new AuthorizationHandlerContext(
new[] { SecurityTaskOperations.Update },
new ClaimsPrincipal(),
task);
await sutProvider.Sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
[Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin), BitAutoData]
public async Task Update_Admin_CanEditCipher_Success(
CurrentContextOrganization organization,
SutProvider<SecurityTaskAuthorizationHandler> sutProvider)
{
var userId = Guid.NewGuid();
var task = new SecurityTask
{
OrganizationId = organization.Id,
CipherId = Guid.NewGuid()
};
var cipherPermissions = new OrganizationCipherPermission
{
Id = task.CipherId.Value,
Edit = true
};
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IGetCipherPermissionsForUserQuery>().GetByOrganization(organization.Id).Returns(new Dictionary<Guid, OrganizationCipherPermission>
{
{ task.CipherId.Value, cipherPermissions }
});
var context = new AuthorizationHandlerContext(
new[] { SecurityTaskOperations.Update },
new ClaimsPrincipal(),
task);
await sutProvider.Sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
[Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin), BitAutoData]
public async Task Read_Admin_ReadonlyCipher_Failure(
CurrentContextOrganization organization,
SutProvider<SecurityTaskAuthorizationHandler> sutProvider)
{
var userId = Guid.NewGuid();
var task = new SecurityTask
{
OrganizationId = organization.Id,
CipherId = Guid.NewGuid()
};
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IGetCipherPermissionsForUserQuery>().GetByOrganization(organization.Id).Returns(new Dictionary<Guid, OrganizationCipherPermission>());
var context = new AuthorizationHandlerContext(
new[] { SecurityTaskOperations.Update },
new ClaimsPrincipal(),
task);
await sutProvider.Sut.HandleAsync(context);
Assert.False(context.HasSucceeded);
}
[Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.User), BitAutoData]
public async Task Update_User_CannotEditCipher_Failure(
CurrentContextOrganization organization,
SutProvider<SecurityTaskAuthorizationHandler> sutProvider)
{
var userId = Guid.NewGuid();
var task = new SecurityTask
{
OrganizationId = organization.Id,
CipherId = Guid.NewGuid()
};
var cipherPermissions = new OrganizationCipherPermission
{
Id = task.CipherId.Value,
Read = true,
Edit = false
};
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IGetCipherPermissionsForUserQuery>().GetByOrganization(organization.Id).Returns(new Dictionary<Guid, OrganizationCipherPermission>
{
{ task.CipherId.Value, cipherPermissions }
});
var context = new AuthorizationHandlerContext(
new[] { SecurityTaskOperations.Update },
new ClaimsPrincipal(),
task);
await sutProvider.Sut.HandleAsync(context);
Assert.False(context.HasSucceeded);
}
}

View File

@ -0,0 +1,104 @@
using System.Security.Claims;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Test.AdminConsole.AutoFixture;
using Bit.Core.Vault.Authorization.SecurityTasks;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Vault.Authorization;
[SutProviderCustomize]
public class SecurityTaskOrganizationAuthorizationHandlerTests
{
[Theory, CurrentContextOrganizationCustomize, BitAutoData]
public async Task MissingOrg_Failure(
CurrentContextOrganization organization,
SutProvider<SecurityTaskOrganizationAuthorizationHandler> sutProvider)
{
var userId = Guid.NewGuid();
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns((CurrentContextOrganization)null);
var context = new AuthorizationHandlerContext(
new[] { SecurityTaskOperations.ListAllForOrganization },
new ClaimsPrincipal(),
organization);
await sutProvider.Sut.HandleAsync(context);
Assert.False(context.HasSucceeded);
}
[Theory, CurrentContextOrganizationCustomize, BitAutoData]
public async Task MissingUserId_Failure(
CurrentContextOrganization organization,
SutProvider<SecurityTaskOrganizationAuthorizationHandler> sutProvider)
{
var userId = Guid.NewGuid();
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(null as Guid?);
var context = new AuthorizationHandlerContext(
new[] { SecurityTaskOperations.ListAllForOrganization },
new ClaimsPrincipal(),
organization);
await sutProvider.Sut.HandleAsync(context);
Assert.False(context.HasSucceeded);
}
[Theory, CurrentContextOrganizationCustomize]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Custom)]
public async Task ListAllForOrganization_Admin_Success(
OrganizationUserType userType,
CurrentContextOrganization organization,
SutProvider<SecurityTaskOrganizationAuthorizationHandler> sutProvider)
{
var userId = Guid.NewGuid();
organization.Type = userType;
if (organization.Type == OrganizationUserType.Custom)
{
organization.Permissions.AccessReports = true;
}
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
var context = new AuthorizationHandlerContext(
new[] { SecurityTaskOperations.ListAllForOrganization },
new ClaimsPrincipal(),
organization);
await sutProvider.Sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
}
[Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.User), BitAutoData]
public async Task ListAllForOrganization_User_Failure(
CurrentContextOrganization organization,
SutProvider<SecurityTaskOrganizationAuthorizationHandler> sutProvider)
{
var userId = Guid.NewGuid();
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
var context = new AuthorizationHandlerContext(
new[] { SecurityTaskOperations.ListAllForOrganization },
new ClaimsPrincipal(),
organization);
await sutProvider.Sut.HandleAsync(context);
Assert.False(context.HasSucceeded);
}
}

View File

@ -0,0 +1,238 @@
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Services;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Queries;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Vault.Queries;
[SutProviderCustomize]
public class GetCipherPermissionsForUserQueryTests
{
private static Guid _noAccessCipherId = Guid.NewGuid();
private static Guid _readOnlyCipherId = Guid.NewGuid();
private static Guid _editCipherId = Guid.NewGuid();
private static Guid _manageCipherId = Guid.NewGuid();
private static Guid _readExceptPasswordCipherId = Guid.NewGuid();
private static Guid _unassignedCipherId = Guid.NewGuid();
private static List<Guid> _cipherIds = new[]
{
_noAccessCipherId,
_readOnlyCipherId,
_editCipherId,
_manageCipherId,
_readExceptPasswordCipherId,
_unassignedCipherId
}.ToList();
[Theory, BitAutoData]
public async Task GetCipherPermissionsForUserQuery_Base(Guid userId, CurrentContextOrganization org, SutProvider<GetCipherPermissionsForUserQuery> sutProvider
)
{
var organizationId = org.Id;
org.Type = OrganizationUserType.User;
org.Permissions.EditAnyCollection = false;
var cipherPermissions = CreateCipherPermissions();
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organizationId).Returns(org);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICipherRepository>().GetCipherPermissionsForOrganizationAsync(organizationId, userId)
.Returns(cipherPermissions);
sutProvider.GetDependency<ICipherRepository>()
.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organizationId)
.Returns(new List<CipherOrganizationDetails>
{
new() { Id = _unassignedCipherId }
});
var result = await sutProvider.Sut.GetByOrganization(organizationId);
Assert.Equal(6, result.Count);
Assert.All(result, x => Assert.Contains(x.Key, _cipherIds));
Assert.False(result[_noAccessCipherId].Read);
Assert.True(result[_readOnlyCipherId].Read);
Assert.False(result[_readOnlyCipherId].Edit);
Assert.True(result[_editCipherId].Edit);
Assert.True(result[_manageCipherId].Manage);
Assert.True(result[_readExceptPasswordCipherId].Read);
Assert.False(result[_readExceptPasswordCipherId].ViewPassword);
Assert.False(result[_unassignedCipherId].Read);
}
[Theory, BitAutoData]
public async Task GetCipherPermissionsForUserQuery_CanEditAllCiphers_CustomUser(Guid userId, CurrentContextOrganization org, SutProvider<GetCipherPermissionsForUserQuery> sutProvider
)
{
var organizationId = org.Id;
var cipherPermissions = CreateCipherPermissions();
org.Permissions.EditAnyCollection = true;
org.Type = OrganizationUserType.Custom;
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organizationId).Returns(org);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICipherRepository>().GetCipherPermissionsForOrganizationAsync(organizationId, userId)
.Returns(cipherPermissions);
sutProvider.GetDependency<ICipherRepository>()
.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organizationId)
.Returns(new List<CipherOrganizationDetails>
{
new() { Id = _unassignedCipherId }
});
var result = await sutProvider.Sut.GetByOrganization(organizationId);
Assert.Equal(6, result.Count);
Assert.All(result, x => Assert.Contains(x.Key, _cipherIds));
Assert.All(result, x => Assert.True(x.Value.Read && x.Value.Edit && x.Value.Manage && x.Value.ViewPassword));
}
[Theory, BitAutoData]
public async Task GetCipherPermissionsForUserQuery_CanEditAllCiphers_Admin(Guid userId, CurrentContextOrganization org, SutProvider<GetCipherPermissionsForUserQuery> sutProvider
)
{
var organizationId = org.Id;
var cipherPermissions = CreateCipherPermissions();
org.Permissions.EditAnyCollection = false;
org.Type = OrganizationUserType.Admin;
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organizationId).Returns(org);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(org.Id).Returns(new OrganizationAbility
{
AllowAdminAccessToAllCollectionItems = true
});
sutProvider.GetDependency<ICipherRepository>().GetCipherPermissionsForOrganizationAsync(organizationId, userId)
.Returns(cipherPermissions);
sutProvider.GetDependency<ICipherRepository>()
.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organizationId)
.Returns(new List<CipherOrganizationDetails>
{
new() { Id = _unassignedCipherId }
});
var result = await sutProvider.Sut.GetByOrganization(organizationId);
Assert.Equal(6, result.Count);
Assert.All(result, x => Assert.Contains(x.Key, _cipherIds));
Assert.All(result, x => Assert.True(x.Value.Read && x.Value.Edit && x.Value.Manage && x.Value.ViewPassword));
}
[Theory, BitAutoData]
public async Task GetCipherPermissionsForUserQuery_CanEditUnassignedCiphers(Guid userId, CurrentContextOrganization org, SutProvider<GetCipherPermissionsForUserQuery> sutProvider
)
{
var organizationId = org.Id;
var cipherPermissions = CreateCipherPermissions();
org.Type = OrganizationUserType.Owner;
org.Permissions.EditAnyCollection = false;
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organizationId).Returns(org);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICipherRepository>().GetCipherPermissionsForOrganizationAsync(organizationId, userId)
.Returns(cipherPermissions);
sutProvider.GetDependency<ICipherRepository>()
.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organizationId)
.Returns(new List<CipherOrganizationDetails>
{
new() { Id = _unassignedCipherId }
});
var result = await sutProvider.Sut.GetByOrganization(organizationId);
Assert.Equal(6, result.Count);
Assert.All(result, x => Assert.Contains(x.Key, _cipherIds));
Assert.False(result[_noAccessCipherId].Read);
Assert.True(result[_readOnlyCipherId].Read);
Assert.False(result[_readOnlyCipherId].Edit);
Assert.True(result[_editCipherId].Edit);
Assert.True(result[_manageCipherId].Manage);
Assert.True(result[_readExceptPasswordCipherId].Read);
Assert.False(result[_readExceptPasswordCipherId].ViewPassword);
Assert.True(result[_unassignedCipherId].Read);
Assert.True(result[_unassignedCipherId].Edit);
Assert.True(result[_unassignedCipherId].ViewPassword);
Assert.True(result[_unassignedCipherId].Manage);
}
private List<OrganizationCipherPermission> CreateCipherPermissions()
{
// User has no relationship with the cipher
var noAccessCipher = new OrganizationCipherPermission
{
Id = _noAccessCipherId,
Read = false,
Edit = false,
Manage = false,
ViewPassword = false,
};
var readOnlyCipher = new OrganizationCipherPermission
{
Id = _readOnlyCipherId,
Read = true,
Edit = false,
Manage = false,
ViewPassword = true,
};
var editCipher = new OrganizationCipherPermission
{
Id = _editCipherId,
Read = true,
Edit = true,
Manage = false,
ViewPassword = true,
};
var manageCipher = new OrganizationCipherPermission
{
Id = _manageCipherId,
Read = true,
Edit = true,
Manage = true,
ViewPassword = true,
};
var readExceptPasswordCipher = new OrganizationCipherPermission
{
Id = _readExceptPasswordCipherId,
Read = true,
Edit = false,
Manage = false,
ViewPassword = false,
};
var unassignedCipher = new OrganizationCipherPermission
{
Id = _unassignedCipherId,
Read = false,
Edit = false,
Manage = false,
ViewPassword = false,
};
return new List<OrganizationCipherPermission>
{
noAccessCipher,
readOnlyCipher,
editCipher,
manageCipher,
readExceptPasswordCipher,
unassignedCipher
};
}
}

View File

@ -1,5 +1,6 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
@ -198,4 +199,238 @@ public class CipherRepositoryTests
Assert.NotEqual(default, userProperty);
Assert.Equal(folder.Id, userProperty.Value.GetGuid());
}
[DatabaseTheory, DatabaseData]
public async Task GetCipherPermissionsForOrganizationAsync_Works(
ICipherRepository cipherRepository,
IUserRepository userRepository,
ICollectionCipherRepository collectionCipherRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IGroupRepository groupRepository
)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test 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,
});
// 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[] { orgUser.Id });
// MANAGE
var manageCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "Manage Collection",
OrganizationId = organization.Id
});
var manageCipher = 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 collectionRepository.UpdateUsersAsync(manageCollection.Id, new List<CollectionAccessSelection>
{
new()
{
Id = orgUser.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
}
});
// EDIT
var editCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "Edit Collection",
OrganizationId = organization.Id
});
var editCipher = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = ""
});
await collectionCipherRepository.UpdateCollectionsForAdminAsync(editCipher.Id, organization.Id,
new List<Guid> { editCollection.Id });
await collectionRepository.UpdateUsersAsync(editCollection.Id,
new List<CollectionAccessSelection>
{
new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = false }
});
// EDIT EXCEPT PASSWORD
var editExceptPasswordCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "Edit Except Password Collection",
OrganizationId = organization.Id
});
var editExceptPasswordCipher = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = ""
});
await collectionCipherRepository.UpdateCollectionsForAdminAsync(editExceptPasswordCipher.Id, organization.Id,
new List<Guid> { editExceptPasswordCollection.Id });
await collectionRepository.UpdateUsersAsync(editExceptPasswordCollection.Id, new List<CollectionAccessSelection>
{
new() { Id = orgUser.Id, HidePasswords = true, ReadOnly = false, Manage = false }
});
// VIEW ONLY
var viewOnlyCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "View Only Collection",
OrganizationId = organization.Id
});
var viewOnlyCipher = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = ""
});
await collectionCipherRepository.UpdateCollectionsForAdminAsync(viewOnlyCipher.Id, organization.Id,
new List<Guid> { viewOnlyCollection.Id });
await collectionRepository.UpdateUsersAsync(viewOnlyCollection.Id,
new List<CollectionAccessSelection>
{
new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = true, Manage = false }
});
// Assign the EditGroup to this View Only collection. The user belongs to this group.
// The user permissions specified above (ViewOnly) should take precedence.
await groupRepository.ReplaceAsync(editGroup,
new[]
{
new CollectionAccessSelection
{
Id = viewOnlyCollection.Id, HidePasswords = false, ReadOnly = false, Manage = false
},
});
// VIEW EXCEPT PASSWORD
var viewExceptPasswordCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "View Except Password Collection",
OrganizationId = organization.Id
});
var viewExceptPasswordCipher = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = ""
});
await collectionCipherRepository.UpdateCollectionsForAdminAsync(viewExceptPasswordCipher.Id, organization.Id,
new List<Guid> { viewExceptPasswordCollection.Id });
await collectionRepository.UpdateUsersAsync(viewExceptPasswordCollection.Id,
new List<CollectionAccessSelection>
{
new() { Id = orgUser.Id, HidePasswords = true, ReadOnly = true, Manage = false }
});
// UNASSIGNED
var unassignedCipher = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = ""
});
var permissions = await cipherRepository.GetCipherPermissionsForOrganizationAsync(organization.Id, user.Id);
Assert.NotEmpty(permissions);
var manageCipherPermission = permissions.FirstOrDefault(c => c.Id == manageCipher.Id);
Assert.NotNull(manageCipherPermission);
Assert.True(manageCipherPermission.Manage);
Assert.True(manageCipherPermission.Edit);
Assert.True(manageCipherPermission.Read);
Assert.True(manageCipherPermission.ViewPassword);
var editCipherPermission = permissions.FirstOrDefault(c => c.Id == editCipher.Id);
Assert.NotNull(editCipherPermission);
Assert.False(editCipherPermission.Manage);
Assert.True(editCipherPermission.Edit);
Assert.True(editCipherPermission.Read);
Assert.True(editCipherPermission.ViewPassword);
var editExceptPasswordCipherPermission = permissions.FirstOrDefault(c => c.Id == editExceptPasswordCipher.Id);
Assert.NotNull(editExceptPasswordCipherPermission);
Assert.False(editExceptPasswordCipherPermission.Manage);
Assert.True(editExceptPasswordCipherPermission.Edit);
Assert.True(editExceptPasswordCipherPermission.Read);
Assert.False(editExceptPasswordCipherPermission.ViewPassword);
var viewOnlyCipherPermission = permissions.FirstOrDefault(c => c.Id == viewOnlyCipher.Id);
Assert.NotNull(viewOnlyCipherPermission);
Assert.False(viewOnlyCipherPermission.Manage);
Assert.False(viewOnlyCipherPermission.Edit);
Assert.True(viewOnlyCipherPermission.Read);
Assert.True(viewOnlyCipherPermission.ViewPassword);
var viewExceptPasswordCipherPermission = permissions.FirstOrDefault(c => c.Id == viewExceptPasswordCipher.Id);
Assert.NotNull(viewExceptPasswordCipherPermission);
Assert.False(viewExceptPasswordCipherPermission.Manage);
Assert.False(viewExceptPasswordCipherPermission.Edit);
Assert.True(viewExceptPasswordCipherPermission.Read);
Assert.False(viewExceptPasswordCipherPermission.ViewPassword);
var unassignedCipherPermission = permissions.FirstOrDefault(c => c.Id == unassignedCipher.Id);
Assert.NotNull(unassignedCipherPermission);
Assert.False(unassignedCipherPermission.Manage);
Assert.False(unassignedCipherPermission.Edit);
Assert.False(unassignedCipherPermission.Read);
Assert.False(unassignedCipherPermission.ViewPassword);
}
}