1
0
mirror of https://github.com/bitwarden/server synced 2026-01-07 11:03:37 +00: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);
}
}