mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[SM-380] Access checks for listing projects (#2496)
* Add project access checks for listing
This commit is contained in:
parent
a7c2ff9dbf
commit
5cd571df64
@ -117,6 +117,10 @@ csharp_new_line_before_members_in_anonymous_types = true
|
|||||||
# Namespace settings
|
# Namespace settings
|
||||||
csharp_style_namespace_declarations = file_scoped:warning
|
csharp_style_namespace_declarations = file_scoped:warning
|
||||||
|
|
||||||
|
# Switch expression
|
||||||
|
dotnet_diagnostic.CS8509.severity = error # missing switch case for named enum value
|
||||||
|
dotnet_diagnostic.CS8524.severity = none # missing switch case for unnamed enum value
|
||||||
|
|
||||||
# All files
|
# All files
|
||||||
[*]
|
[*]
|
||||||
guidelines = 120
|
guidelines = 120
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
using Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
||||||
@ -8,36 +10,65 @@ namespace Bit.Commercial.Core.SecretManagerFeatures.Projects;
|
|||||||
public class DeleteProjectCommand : IDeleteProjectCommand
|
public class DeleteProjectCommand : IDeleteProjectCommand
|
||||||
{
|
{
|
||||||
private readonly IProjectRepository _projectRepository;
|
private readonly IProjectRepository _projectRepository;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
|
||||||
public DeleteProjectCommand(IProjectRepository projectRepository)
|
public DeleteProjectCommand(IProjectRepository projectRepository, ICurrentContext currentContext)
|
||||||
{
|
{
|
||||||
_projectRepository = projectRepository;
|
_projectRepository = projectRepository;
|
||||||
|
_currentContext = currentContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Tuple<Project, string>>> DeleteProjects(List<Guid> ids)
|
public async Task<List<Tuple<Project, string>>> DeleteProjects(List<Guid> ids, Guid userId)
|
||||||
{
|
{
|
||||||
var projects = await _projectRepository.GetManyByIds(ids);
|
if (ids.Any() != true || userId == new Guid())
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException();
|
||||||
|
}
|
||||||
|
|
||||||
if (projects?.Any() != true)
|
var projects = (await _projectRepository.GetManyByIds(ids))?.ToList();
|
||||||
|
|
||||||
|
if (projects?.Any() != true || projects.Count != ids.Count)
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var results = ids.Select(id =>
|
// Ensure all projects belongs to the same organization
|
||||||
|
var organizationId = projects.First().OrganizationId;
|
||||||
|
if (projects.Any(p => p.OrganizationId != organizationId))
|
||||||
{
|
{
|
||||||
var project = projects.FirstOrDefault(project => project.Id == id);
|
throw new UnauthorizedAccessException();
|
||||||
if (project == null)
|
}
|
||||||
|
|
||||||
|
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
|
||||||
|
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
|
||||||
|
|
||||||
|
var results = new List<Tuple<Project, String>>(projects.Count);
|
||||||
|
var deleteIds = new List<Guid>();
|
||||||
|
|
||||||
|
foreach (var project in projects)
|
||||||
|
{
|
||||||
|
var hasAccess = accessClient switch
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
AccessClientType.NoAccessCheck => true,
|
||||||
|
AccessClientType.User => await _projectRepository.UserHasWriteAccessToProject(project.Id, userId),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hasAccess)
|
||||||
|
{
|
||||||
|
results.Add(new Tuple<Project, string>(project, "access denied"));
|
||||||
}
|
}
|
||||||
// TODO Once permissions are implemented add check for each project here.
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return new Tuple<Project, string>(project, "");
|
results.Add(new Tuple<Project, string>(project, ""));
|
||||||
|
deleteIds.Add(project.Id);
|
||||||
}
|
}
|
||||||
}).ToList();
|
}
|
||||||
|
|
||||||
await _projectRepository.DeleteManyByIdAsync(ids);
|
if (deleteIds.Count > 0)
|
||||||
|
{
|
||||||
|
await _projectRepository.DeleteManyByIdAsync(deleteIds);
|
||||||
|
}
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
using Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
||||||
@ -8,23 +10,38 @@ namespace Bit.Commercial.Core.SecretManagerFeatures.Projects;
|
|||||||
public class UpdateProjectCommand : IUpdateProjectCommand
|
public class UpdateProjectCommand : IUpdateProjectCommand
|
||||||
{
|
{
|
||||||
private readonly IProjectRepository _projectRepository;
|
private readonly IProjectRepository _projectRepository;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
|
||||||
public UpdateProjectCommand(IProjectRepository projectRepository)
|
public UpdateProjectCommand(IProjectRepository projectRepository, ICurrentContext currentContext)
|
||||||
{
|
{
|
||||||
_projectRepository = projectRepository;
|
_projectRepository = projectRepository;
|
||||||
|
_currentContext = currentContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Project> UpdateAsync(Project project)
|
public async Task<Project> UpdateAsync(Project updatedProject, Guid userId)
|
||||||
{
|
{
|
||||||
var existingProject = await _projectRepository.GetByIdAsync(project.Id);
|
var project = await _projectRepository.GetByIdAsync(updatedProject.Id);
|
||||||
if (existingProject == null)
|
if (project == null)
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
project.OrganizationId = existingProject.OrganizationId;
|
var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId);
|
||||||
project.CreationDate = existingProject.CreationDate;
|
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
|
||||||
project.DeletedDate = existingProject.DeletedDate;
|
|
||||||
|
var hasAccess = accessClient switch
|
||||||
|
{
|
||||||
|
AccessClientType.NoAccessCheck => true,
|
||||||
|
AccessClientType.User => await _projectRepository.UserHasWriteAccessToProject(updatedProject.Id, userId),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hasAccess)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
project.Name = updatedProject.Name;
|
||||||
project.RevisionDate = DateTime.UtcNow;
|
project.RevisionDate = DateTime.UtcNow;
|
||||||
|
|
||||||
await _projectRepository.ReplaceAsync(project);
|
await _projectRepository.ReplaceAsync(project);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using Bit.Core.Enums;
|
||||||
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;
|
||||||
@ -26,29 +27,40 @@ public class ProjectRepository : Repository<Core.Entities.Project, Project, Guid
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Core.Entities.Project>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId)
|
public async Task<IEnumerable<Core.Entities.Project>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
|
||||||
{
|
{
|
||||||
using var scope = ServiceScopeFactory.CreateScope();
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
var project = await dbContext.Project
|
var query = dbContext.Project.Where(p => p.OrganizationId == organizationId && p.DeletedDate == null);
|
||||||
.Where(p => p.OrganizationId == organizationId && p.DeletedDate == null)
|
|
||||||
// TODO: Enable this + Handle Admins
|
query = accessType switch
|
||||||
//.Where(UserHasAccessToProject(userId))
|
{
|
||||||
.OrderBy(p => p.RevisionDate)
|
AccessClientType.NoAccessCheck => query,
|
||||||
.ToListAsync();
|
AccessClientType.User => query.Where(UserHasReadAccessToProject(userId)),
|
||||||
return Mapper.Map<List<Core.Entities.Project>>(project);
|
AccessClientType.ServiceAccount => query.Where(ServiceAccountHasReadAccessToProject(userId)),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),
|
||||||
|
};
|
||||||
|
|
||||||
|
var projects = await query.OrderBy(p => p.RevisionDate).ToListAsync();
|
||||||
|
return Mapper.Map<List<Core.Entities.Project>>(projects);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Expression<Func<Project, bool>> UserHasAccessToProject(Guid userId) => p =>
|
private static Expression<Func<Project, bool>> UserHasReadAccessToProject(Guid userId) => p =>
|
||||||
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
||||||
p.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));
|
p.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));
|
||||||
|
|
||||||
|
private static Expression<Func<Project, bool>> UserHasWriteAccessToProject(Guid userId) => p =>
|
||||||
|
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
|
||||||
|
p.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write));
|
||||||
|
|
||||||
|
private static Expression<Func<Project, bool>> ServiceAccountHasReadAccessToProject(Guid serviceAccountId) => p =>
|
||||||
|
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Read);
|
||||||
|
|
||||||
public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
|
public async Task DeleteManyByIdAsync(IEnumerable<Guid> ids)
|
||||||
{
|
{
|
||||||
using (var scope = ServiceScopeFactory.CreateScope())
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
{
|
{
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
var utcNow = DateTime.UtcNow;
|
|
||||||
var projects = dbContext.Project.Where(c => ids.Contains(c.Id));
|
var projects = dbContext.Project.Where(c => ids.Contains(c.Id));
|
||||||
await projects.ForEachAsync(project =>
|
await projects.ForEachAsync(project =>
|
||||||
{
|
{
|
||||||
@ -68,6 +80,27 @@ public class ProjectRepository : Repository<Core.Entities.Project, Project, Guid
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
return Mapper.Map<List<Core.Entities.Project>>(projects);
|
return Mapper.Map<List<Core.Entities.Project>>(projects);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UserHasReadAccessToProject(Guid id, Guid userId)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var query = dbContext.Project
|
||||||
|
.Where(p => p.Id == id)
|
||||||
|
.Where(UserHasReadAccessToProject(userId));
|
||||||
|
|
||||||
|
return await query.AnyAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UserHasWriteAccessToProject(Guid id, Guid userId)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var query = dbContext.Project
|
||||||
|
.Where(p => p.Id == id)
|
||||||
|
.Where(UserHasWriteAccessToProject(userId));
|
||||||
|
|
||||||
|
return await query.AnyAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
using Bit.Commercial.Core.SecretManagerFeatures.Projects;
|
using Bit.Commercial.Core.SecretManagerFeatures.Projects;
|
||||||
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Identity;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
@ -14,19 +16,19 @@ public class DeleteProjectCommandTests
|
|||||||
{
|
{
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task DeleteProjects_Throws_NotFoundException(List<Guid> data,
|
public async Task DeleteProjects_Throws_NotFoundException(List<Guid> data, Guid userId,
|
||||||
SutProvider<DeleteProjectCommand> sutProvider)
|
SutProvider<DeleteProjectCommand> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(new List<Project>());
|
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(new List<Project>());
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteProjects(data));
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteProjects(data, userId));
|
||||||
|
|
||||||
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs().DeleteManyByIdAsync(default);
|
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs().DeleteManyByIdAsync(default);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task DeleteSecrets_OneIdNotFound_Throws_NotFoundException(List<Guid> data,
|
public async Task DeleteSecrets_OneIdNotFound_Throws_NotFoundException(List<Guid> data, Guid userId,
|
||||||
SutProvider<DeleteProjectCommand> sutProvider)
|
SutProvider<DeleteProjectCommand> sutProvider)
|
||||||
{
|
{
|
||||||
var project = new Project()
|
var project = new Project()
|
||||||
@ -35,35 +37,69 @@ public class DeleteProjectCommandTests
|
|||||||
};
|
};
|
||||||
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(new List<Project>() { project });
|
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(new List<Project>() { project });
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteProjects(data));
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.DeleteProjects(data, userId));
|
||||||
|
|
||||||
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs().DeleteManyByIdAsync(default);
|
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs().DeleteManyByIdAsync(default);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task DeleteSecrets_Success(List<Guid> data,
|
public async Task DeleteSecrets_User_Success(List<Guid> data, Guid userId, Guid organizationId,
|
||||||
SutProvider<DeleteProjectCommand> sutProvider)
|
SutProvider<DeleteProjectCommand> sutProvider)
|
||||||
{
|
{
|
||||||
var projects = new List<Project>();
|
var projects = data.Select(id => new Project { Id = id, OrganizationId = organizationId }).ToList();
|
||||||
foreach (Guid id in data)
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ClientType = ClientType.User;
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(projects);
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(Arg.Any<Guid>(), userId).Returns(true);
|
||||||
|
|
||||||
|
var results = await sutProvider.Sut.DeleteProjects(data, userId);
|
||||||
|
|
||||||
|
foreach (var result in results)
|
||||||
{
|
{
|
||||||
var project = new Project()
|
Assert.Equal("", result.Item2);
|
||||||
{
|
|
||||||
Id = id
|
|
||||||
};
|
|
||||||
projects.Add(project);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IProjectRepository>().Received(1).DeleteManyByIdAsync(Arg.Is<List<Guid>>(d => d.SequenceEqual(data)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task DeleteSecrets_User_No_Permission(List<Guid> data, Guid userId, Guid organizationId,
|
||||||
|
SutProvider<DeleteProjectCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var projects = data.Select(id => new Project { Id = id, OrganizationId = organizationId }).ToList();
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ClientType = ClientType.User;
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(projects);
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(userId, userId).Returns(false);
|
||||||
|
|
||||||
|
var results = await sutProvider.Sut.DeleteProjects(data, userId);
|
||||||
|
|
||||||
|
foreach (var result in results)
|
||||||
|
{
|
||||||
|
Assert.Equal("access denied", result.Item2);
|
||||||
|
}
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs().DeleteManyByIdAsync(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task DeleteSecrets_OrganizationAdmin_Success(List<Guid> data, Guid userId, Guid organizationId,
|
||||||
|
SutProvider<DeleteProjectCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var projects = data.Select(id => new Project { Id = id, OrganizationId = organizationId }).ToList();
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(organizationId).Returns(true);
|
||||||
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(projects);
|
sutProvider.GetDependency<IProjectRepository>().GetManyByIds(data).Returns(projects);
|
||||||
|
|
||||||
var results = await sutProvider.Sut.DeleteProjects(data);
|
var results = await sutProvider.Sut.DeleteProjects(data, userId);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IProjectRepository>().Received(1).DeleteManyByIdAsync(Arg.Is(data));
|
await sutProvider.GetDependency<IProjectRepository>().Received(1).DeleteManyByIdAsync(Arg.Is<List<Guid>>(d => d.SequenceEqual(data)));
|
||||||
foreach (var result in results)
|
foreach (var result in results)
|
||||||
{
|
{
|
||||||
Assert.Equal("", result.Item2);
|
Assert.Equal("", result.Item2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
using Bit.Commercial.Core.SecretManagerFeatures.Projects;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Test.AutoFixture.ProjectsFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Bit.Test.Common.Helpers;
|
||||||
|
using NSubstitute;
|
||||||
|
using NSubstitute.ReturnsExtensions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Commercial.Core.Test.SecretManagerFeatures.Projects;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
[ProjectCustomize]
|
||||||
|
public class UpdateProjectCommandTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task UpdateAsync_Throws_NotFoundException(Project project, Guid userId, SutProvider<UpdateProjectCommand> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).ReturnsNull();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(project, userId));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task UpdateAsync_Admin_Succeeds(Project project, Guid userId, SutProvider<UpdateProjectCommand> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(project.OrganizationId).Returns(true);
|
||||||
|
|
||||||
|
var project2 = new Project { Id = project.Id, Name = "newName" };
|
||||||
|
var result = await sutProvider.Sut.UpdateAsync(project2, userId);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal("newName", result.Name);
|
||||||
|
AssertHelper.AssertRecent(result.RevisionDate);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IProjectRepository>().ReceivedWithAnyArgs(1).ReplaceAsync(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task UpdateAsync_User_NoAccess(Project project, Guid userId, SutProvider<UpdateProjectCommand> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(project.Id, userId).Returns(false);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.UpdateAsync(project, userId));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task UpdateAsync_User_Success(Project project, Guid userId, SutProvider<UpdateProjectCommand> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().GetByIdAsync(project.Id).Returns(project);
|
||||||
|
sutProvider.GetDependency<IProjectRepository>().UserHasWriteAccessToProject(project.Id, userId).Returns(true);
|
||||||
|
|
||||||
|
var project2 = new Project { Id = project.Id, Name = "newName" };
|
||||||
|
var result = await sutProvider.Sut.UpdateAsync(project2, userId);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal("newName", result.Name);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IProjectRepository>().ReceivedWithAnyArgs(1).ReplaceAsync(default);
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,8 @@
|
|||||||
using Bit.Api.SecretManagerFeatures.Models.Request;
|
using Bit.Api.SecretManagerFeatures.Models.Request;
|
||||||
using Bit.Api.SecretManagerFeatures.Models.Response;
|
using Bit.Api.SecretManagerFeatures.Models.Response;
|
||||||
using Bit.Api.Utilities;
|
using Bit.Api.Utilities;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
using Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
||||||
@ -18,24 +20,32 @@ public class ProjectsController : Controller
|
|||||||
private readonly ICreateProjectCommand _createProjectCommand;
|
private readonly ICreateProjectCommand _createProjectCommand;
|
||||||
private readonly IUpdateProjectCommand _updateProjectCommand;
|
private readonly IUpdateProjectCommand _updateProjectCommand;
|
||||||
private readonly IDeleteProjectCommand _deleteProjectCommand;
|
private readonly IDeleteProjectCommand _deleteProjectCommand;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
|
||||||
public ProjectsController(
|
public ProjectsController(
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IProjectRepository projectRepository,
|
IProjectRepository projectRepository,
|
||||||
ICreateProjectCommand createProjectCommand,
|
ICreateProjectCommand createProjectCommand,
|
||||||
IUpdateProjectCommand updateProjectCommand,
|
IUpdateProjectCommand updateProjectCommand,
|
||||||
IDeleteProjectCommand deleteProjectCommand)
|
IDeleteProjectCommand deleteProjectCommand,
|
||||||
|
ICurrentContext currentContext)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_projectRepository = projectRepository;
|
_projectRepository = projectRepository;
|
||||||
_createProjectCommand = createProjectCommand;
|
_createProjectCommand = createProjectCommand;
|
||||||
_updateProjectCommand = updateProjectCommand;
|
_updateProjectCommand = updateProjectCommand;
|
||||||
_deleteProjectCommand = deleteProjectCommand;
|
_deleteProjectCommand = deleteProjectCommand;
|
||||||
|
_currentContext = currentContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("organizations/{organizationId}/projects")]
|
[HttpPost("organizations/{organizationId}/projects")]
|
||||||
public async Task<ProjectResponseModel> CreateAsync([FromRoute] Guid organizationId, [FromBody] ProjectCreateRequestModel createRequest)
|
public async Task<ProjectResponseModel> CreateAsync([FromRoute] Guid organizationId, [FromBody] ProjectCreateRequestModel createRequest)
|
||||||
{
|
{
|
||||||
|
if (!await _currentContext.OrganizationUser(organizationId))
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
var result = await _createProjectCommand.CreateAsync(createRequest.ToProject(organizationId));
|
var result = await _createProjectCommand.CreateAsync(createRequest.ToProject(organizationId));
|
||||||
return new ProjectResponseModel(result);
|
return new ProjectResponseModel(result);
|
||||||
}
|
}
|
||||||
@ -43,15 +53,22 @@ public class ProjectsController : Controller
|
|||||||
[HttpPut("projects/{id}")]
|
[HttpPut("projects/{id}")]
|
||||||
public async Task<ProjectResponseModel> UpdateProjectAsync([FromRoute] Guid id, [FromBody] ProjectUpdateRequestModel updateRequest)
|
public async Task<ProjectResponseModel> UpdateProjectAsync([FromRoute] Guid id, [FromBody] ProjectUpdateRequestModel updateRequest)
|
||||||
{
|
{
|
||||||
var result = await _updateProjectCommand.UpdateAsync(updateRequest.ToProject(id));
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
|
|
||||||
|
var result = await _updateProjectCommand.UpdateAsync(updateRequest.ToProject(id), userId);
|
||||||
return new ProjectResponseModel(result);
|
return new ProjectResponseModel(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("organizations/{organizationId}/projects")]
|
[HttpGet("organizations/{organizationId}/projects")]
|
||||||
public async Task<ListResponseModel<ProjectResponseModel>> GetProjectsByOrganizationAsync([FromRoute] Guid organizationId)
|
public async Task<ListResponseModel<ProjectResponseModel>> GetProjectsByOrganizationAsync(
|
||||||
|
[FromRoute] Guid organizationId)
|
||||||
{
|
{
|
||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
var projects = await _projectRepository.GetManyByOrganizationIdAsync(organizationId, userId);
|
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
|
||||||
|
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
|
||||||
|
|
||||||
|
var projects = await _projectRepository.GetManyByOrganizationIdAsync(organizationId, userId, accessClient);
|
||||||
|
|
||||||
var responses = projects.Select(project => new ProjectResponseModel(project));
|
var responses = projects.Select(project => new ProjectResponseModel(project));
|
||||||
return new ListResponseModel<ProjectResponseModel>(responses);
|
return new ListResponseModel<ProjectResponseModel>(responses);
|
||||||
}
|
}
|
||||||
@ -64,13 +81,32 @@ public class ProjectsController : Controller
|
|||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
|
var orgAdmin = await _currentContext.OrganizationAdmin(project.OrganizationId);
|
||||||
|
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
|
||||||
|
|
||||||
|
var hasAccess = accessClient switch
|
||||||
|
{
|
||||||
|
AccessClientType.NoAccessCheck => true,
|
||||||
|
AccessClientType.User => await _projectRepository.UserHasReadAccessToProject(id, userId),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hasAccess)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
return new ProjectResponseModel(project);
|
return new ProjectResponseModel(project);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("projects/delete")]
|
[HttpPost("projects/delete")]
|
||||||
public async Task<ListResponseModel<BulkDeleteResponseModel>> BulkDeleteProjectsAsync([FromBody] List<Guid> ids)
|
public async Task<ListResponseModel<BulkDeleteResponseModel>> BulkDeleteProjectsAsync([FromBody] List<Guid> ids)
|
||||||
{
|
{
|
||||||
var results = await _deleteProjectCommand.DeleteProjects(ids);
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
|
|
||||||
|
var results = await _deleteProjectCommand.DeleteProjects(ids, userId);
|
||||||
var responses = results.Select(r => new BulkDeleteResponseModel(r.Item1.Id, r.Item2));
|
var responses = results.Select(r => new BulkDeleteResponseModel(r.Item1.Id, r.Item2));
|
||||||
return new ListResponseModel<BulkDeleteResponseModel>(responses);
|
return new ListResponseModel<BulkDeleteResponseModel>(responses);
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ public class CurrentContext : ICurrentContext
|
|||||||
public virtual int? BotScore { get; set; }
|
public virtual int? BotScore { get; set; }
|
||||||
public virtual string ClientId { get; set; }
|
public virtual string ClientId { get; set; }
|
||||||
public virtual Version ClientVersion { get; set; }
|
public virtual Version ClientVersion { get; set; }
|
||||||
|
public virtual ClientType ClientType { get; set; }
|
||||||
|
|
||||||
public CurrentContext(IProviderUserRepository providerUserRepository)
|
public CurrentContext(IProviderUserRepository providerUserRepository)
|
||||||
{
|
{
|
||||||
@ -138,6 +139,13 @@ public class CurrentContext : ICurrentContext
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var clientType = GetClaimValue(claimsDict, Claims.Type);
|
||||||
|
if (clientType != null)
|
||||||
|
{
|
||||||
|
Enum.TryParse(clientType, out ClientType c);
|
||||||
|
ClientType = c;
|
||||||
|
}
|
||||||
|
|
||||||
DeviceIdentifier = GetClaimValue(claimsDict, Claims.Device);
|
DeviceIdentifier = GetClaimValue(claimsDict, Claims.Device);
|
||||||
|
|
||||||
Organizations = GetOrganizations(claimsDict, orgApi);
|
Organizations = GetOrganizations(claimsDict, orgApi);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Identity;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@ -18,6 +19,7 @@ public interface ICurrentContext
|
|||||||
List<CurrentContentOrganization> Organizations { get; set; }
|
List<CurrentContentOrganization> Organizations { get; set; }
|
||||||
Guid? InstallationId { get; set; }
|
Guid? InstallationId { get; set; }
|
||||||
Guid? OrganizationId { get; set; }
|
Guid? OrganizationId { get; set; }
|
||||||
|
ClientType ClientType { get; set; }
|
||||||
bool IsBot { get; set; }
|
bool IsBot { get; set; }
|
||||||
bool MaybeBot { get; set; }
|
bool MaybeBot { get; set; }
|
||||||
int? BotScore { get; set; }
|
int? BotScore { get; set; }
|
||||||
|
30
src/Core/Enums/AccessClientType.cs
Normal file
30
src/Core/Enums/AccessClientType.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using Bit.Core.Identity;
|
||||||
|
|
||||||
|
namespace Bit.Core.Enums;
|
||||||
|
|
||||||
|
public enum AccessClientType
|
||||||
|
{
|
||||||
|
NoAccessCheck = -1,
|
||||||
|
User = 0,
|
||||||
|
Organization = 1,
|
||||||
|
ServiceAccount = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AccessClientHelper
|
||||||
|
{
|
||||||
|
public static AccessClientType ToAccessClient(ClientType clientType, bool bypassAccessCheck = false)
|
||||||
|
{
|
||||||
|
if (bypassAccessCheck)
|
||||||
|
{
|
||||||
|
return AccessClientType.NoAccessCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientType switch
|
||||||
|
{
|
||||||
|
ClientType.User => AccessClientType.User,
|
||||||
|
ClientType.Organization => AccessClientType.Organization,
|
||||||
|
ClientType.ServiceAccount => AccessClientType.ServiceAccount,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(clientType), clientType, null),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -16,4 +16,7 @@ public static class Claims
|
|||||||
|
|
||||||
// Service Account
|
// Service Account
|
||||||
public const string Organization = "organization";
|
public const string Organization = "organization";
|
||||||
|
|
||||||
|
// General
|
||||||
|
public const string Type = "type";
|
||||||
}
|
}
|
||||||
|
8
src/Core/Identity/ClientType.cs
Normal file
8
src/Core/Identity/ClientType.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Bit.Core.Identity;
|
||||||
|
|
||||||
|
public enum ClientType : byte
|
||||||
|
{
|
||||||
|
User = 0,
|
||||||
|
Organization = 1,
|
||||||
|
ServiceAccount = 2,
|
||||||
|
}
|
@ -1,13 +1,16 @@
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.Repositories;
|
namespace Bit.Core.Repositories;
|
||||||
|
|
||||||
public interface IProjectRepository
|
public interface IProjectRepository
|
||||||
{
|
{
|
||||||
Task<IEnumerable<Project>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId);
|
Task<IEnumerable<Project>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);
|
||||||
Task<IEnumerable<Project>> GetManyByIds(IEnumerable<Guid> ids);
|
Task<IEnumerable<Project>> GetManyByIds(IEnumerable<Guid> ids);
|
||||||
Task<Project> GetByIdAsync(Guid id);
|
Task<Project> GetByIdAsync(Guid id);
|
||||||
Task<Project> CreateAsync(Project project);
|
Task<Project> CreateAsync(Project project);
|
||||||
Task ReplaceAsync(Project project);
|
Task ReplaceAsync(Project project);
|
||||||
Task DeleteManyByIdAsync(IEnumerable<Guid> ids);
|
Task DeleteManyByIdAsync(IEnumerable<Guid> ids);
|
||||||
|
Task<bool> UserHasReadAccessToProject(Guid id, Guid userId);
|
||||||
|
Task<bool> UserHasWriteAccessToProject(Guid id, Guid userId);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,6 @@ namespace Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
|||||||
|
|
||||||
public interface IDeleteProjectCommand
|
public interface IDeleteProjectCommand
|
||||||
{
|
{
|
||||||
Task<List<Tuple<Project, string>>> DeleteProjects(List<Guid> ids);
|
Task<List<Tuple<Project, string>>> DeleteProjects(List<Guid> ids, Guid userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,5 +4,5 @@ namespace Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
|||||||
|
|
||||||
public interface IUpdateProjectCommand
|
public interface IUpdateProjectCommand
|
||||||
{
|
{
|
||||||
Task<Project> UpdateAsync(Project project);
|
Task<Project> UpdateAsync(Project updatedProject, Guid userId);
|
||||||
}
|
}
|
||||||
|
@ -110,6 +110,7 @@ public class ClientStore : IClientStore
|
|||||||
Claims = new List<ClientClaim>
|
Claims = new List<ClientClaim>
|
||||||
{
|
{
|
||||||
new(JwtClaimTypes.Subject, apiKey.ServiceAccountId.ToString()),
|
new(JwtClaimTypes.Subject, apiKey.ServiceAccountId.ToString()),
|
||||||
|
new(Claims.Type, ClientType.ServiceAccount.ToString()),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -141,6 +142,7 @@ public class ClientStore : IClientStore
|
|||||||
{
|
{
|
||||||
new(JwtClaimTypes.Subject, user.Id.ToString()),
|
new(JwtClaimTypes.Subject, user.Id.ToString()),
|
||||||
new(JwtClaimTypes.AuthenticationMethod, "Application", "external"),
|
new(JwtClaimTypes.AuthenticationMethod, "Application", "external"),
|
||||||
|
new(Claims.Type, ClientType.User.ToString()),
|
||||||
};
|
};
|
||||||
var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id);
|
var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id);
|
||||||
var providers = await _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id);
|
var providers = await _currentContext.ProviderMembershipAsync(_providerUserRepository, user.Id);
|
||||||
@ -198,6 +200,7 @@ public class ClientStore : IClientStore
|
|||||||
Claims = new List<ClientClaim>
|
Claims = new List<ClientClaim>
|
||||||
{
|
{
|
||||||
new(JwtClaimTypes.Subject, org.Id.ToString()),
|
new(JwtClaimTypes.Subject, org.Id.ToString()),
|
||||||
|
new(Claims.Type, ClientType.Organization.ToString()),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
using System.Net.Http.Headers;
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
using Bit.Api.IntegrationTest.Factories;
|
using Bit.Api.IntegrationTest.Factories;
|
||||||
using Bit.Api.IntegrationTest.Helpers;
|
using Bit.Api.IntegrationTest.Helpers;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Api.SecretManagerFeatures.Models.Request;
|
using Bit.Api.SecretManagerFeatures.Models.Request;
|
||||||
using Bit.Api.SecretManagerFeatures.Models.Response;
|
using Bit.Api.SecretManagerFeatures.Models.Response;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Test.Common.Helpers;
|
using Bit.Test.Common.Helpers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@ -31,10 +33,19 @@ public class ProjectsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
|
|||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
var ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
var ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||||
var tokens = await _factory.LoginWithNewAccount(ownerEmail);
|
await _factory.LoginWithNewAccount(ownerEmail);
|
||||||
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, ownerEmail: ownerEmail, billingEmail: ownerEmail);
|
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, ownerEmail: ownerEmail, billingEmail: ownerEmail);
|
||||||
|
var tokens = await _factory.LoginAsync(ownerEmail);
|
||||||
|
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoginAsNewOrgUser(OrganizationUserType type = OrganizationUserType.User)
|
||||||
|
{
|
||||||
|
var email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
await _factory.LoginWithNewAccount(email);
|
||||||
|
await OrganizationTestHelpers.CreateUserAsync(_factory, _organization.Id, email, type);
|
||||||
|
var tokens = await _factory.LoginAsync(email);
|
||||||
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
|
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
|
||||||
_organization = organization;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task DisposeAsync()
|
public Task DisposeAsync()
|
||||||
@ -44,12 +55,9 @@ public class ProjectsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CreateProject()
|
public async Task CreateProject_Success()
|
||||||
{
|
{
|
||||||
var request = new ProjectCreateRequestModel()
|
var request = new ProjectCreateRequestModel { Name = _mockEncryptedString };
|
||||||
{
|
|
||||||
Name = _mockEncryptedString
|
|
||||||
};
|
|
||||||
|
|
||||||
var response = await _client.PostAsJsonAsync($"/organizations/{_organization.Id}/projects", request);
|
var response = await _client.PostAsJsonAsync($"/organizations/{_organization.Id}/projects", request);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
@ -69,7 +77,17 @@ public class ProjectsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task UpdateProject()
|
public async Task CreateProject_NoPermission()
|
||||||
|
{
|
||||||
|
var request = new ProjectCreateRequestModel { Name = _mockEncryptedString };
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync("/organizations/911d9106-7cf1-4d55-a3f9-f9abdeadecb3/projects", request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateProject_Success()
|
||||||
{
|
{
|
||||||
var initialProject = await _projectRepository.CreateAsync(new Project
|
var initialProject = await _projectRepository.CreateAsync(new Project
|
||||||
{
|
{
|
||||||
@ -101,6 +119,42 @@ public class ProjectsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
|
|||||||
Assert.NotEqual(initialProject.RevisionDate, updatedProject.RevisionDate);
|
Assert.NotEqual(initialProject.RevisionDate, updatedProject.RevisionDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateProject_NotFound()
|
||||||
|
{
|
||||||
|
var request = new ProjectUpdateRequestModel()
|
||||||
|
{
|
||||||
|
Name = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=",
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _client.PutAsJsonAsync("/projects/c53de509-4581-402c-8cbd-f26d2c516fba", request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateProject_MissingPermission()
|
||||||
|
{
|
||||||
|
// Create a new account as a user
|
||||||
|
await LoginAsNewOrgUser();
|
||||||
|
|
||||||
|
var project = await _projectRepository.CreateAsync(new Project
|
||||||
|
{
|
||||||
|
OrganizationId = _organization.Id,
|
||||||
|
Name = _mockEncryptedString
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var request = new ProjectUpdateRequestModel()
|
||||||
|
{
|
||||||
|
Name = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=",
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _client.PutAsJsonAsync($"/projects/{project.Id}", request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetProject()
|
public async Task GetProject()
|
||||||
{
|
{
|
||||||
|
@ -45,4 +45,12 @@ public class ApiApplicationFactory : WebApplicationFactoryBase<Startup>
|
|||||||
|
|
||||||
return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash);
|
return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper for logging in to an account
|
||||||
|
/// </summary>
|
||||||
|
public async Task<(string Token, string RefreshToken)> LoginAsync(string email = "integration-test@bitwarden.com", string masterPasswordHash = "master_password_hash")
|
||||||
|
{
|
||||||
|
return await _identityApplicationFactory.TokenFromPasswordAsync(email, masterPasswordHash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,8 @@ namespace Bit.Api.IntegrationTest.Helpers;
|
|||||||
|
|
||||||
public static class OrganizationTestHelpers
|
public static class OrganizationTestHelpers
|
||||||
{
|
{
|
||||||
public static async Task<Tuple<Organization, OrganizationUser>> SignUpAsync<T>(WebApplicationFactoryBase<T> factory,
|
public static async Task<Tuple<Organization, OrganizationUser>> SignUpAsync<T>(
|
||||||
|
WebApplicationFactoryBase<T> factory,
|
||||||
PlanType plan = PlanType.Free,
|
PlanType plan = PlanType.Free,
|
||||||
string ownerEmail = "integration-test@bitwarden.com",
|
string ownerEmail = "integration-test@bitwarden.com",
|
||||||
string name = "Integration Test Org",
|
string name = "Integration Test Org",
|
||||||
@ -30,4 +31,32 @@ public static class OrganizationTestHelpers
|
|||||||
Owner = owner,
|
Owner = owner,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<OrganizationUser> CreateUserAsync<T>(
|
||||||
|
WebApplicationFactoryBase<T> factory,
|
||||||
|
Guid organizationId,
|
||||||
|
string userEmail,
|
||||||
|
OrganizationUserType type
|
||||||
|
) where T : class
|
||||||
|
{
|
||||||
|
var userRepository = factory.GetService<IUserRepository>();
|
||||||
|
var organizationUserRepository = factory.GetService<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
var user = await userRepository.GetByEmailAsync(userEmail);
|
||||||
|
|
||||||
|
var orgUser = new OrganizationUser
|
||||||
|
{
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
UserId = user.Id,
|
||||||
|
Key = null,
|
||||||
|
Type = type,
|
||||||
|
Status = OrganizationUserStatusType.Invited,
|
||||||
|
AccessAll = false,
|
||||||
|
ExternalId = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await organizationUserRepository.CreateAsync(orgUser);
|
||||||
|
|
||||||
|
return orgUser;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using Bit.Api.Controllers;
|
using Bit.Api.Controllers;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
using Bit.Core.SecretManagerFeatures.Projects.Interfaces;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Test.AutoFixture.ProjectsFixture;
|
using Bit.Core.Test.AutoFixture.ProjectsFixture;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
@ -19,17 +20,18 @@ public class ProjectsControllerTests
|
|||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async void BulkDeleteProjects_Success(SutProvider<ProjectsController> sutProvider, List<Project> data)
|
public async void BulkDeleteProjects_Success(SutProvider<ProjectsController> sutProvider, List<Project> data)
|
||||||
{
|
{
|
||||||
var ids = data.Select(project => project.Id).ToList();
|
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
|
||||||
|
var ids = data.Select(project => project.Id)?.ToList();
|
||||||
var mockResult = new List<Tuple<Project, string>>();
|
var mockResult = new List<Tuple<Project, string>>();
|
||||||
foreach (var project in data)
|
foreach (var project in data)
|
||||||
{
|
{
|
||||||
mockResult.Add(new Tuple<Project, string>(project, ""));
|
mockResult.Add(new Tuple<Project, string>(project, ""));
|
||||||
}
|
}
|
||||||
sutProvider.GetDependency<IDeleteProjectCommand>().DeleteProjects(ids).ReturnsForAnyArgs(mockResult);
|
sutProvider.GetDependency<IDeleteProjectCommand>().DeleteProjects(ids, default).ReturnsForAnyArgs(mockResult);
|
||||||
|
|
||||||
var results = await sutProvider.Sut.BulkDeleteProjectsAsync(ids);
|
var results = await sutProvider.Sut.BulkDeleteProjectsAsync(ids);
|
||||||
await sutProvider.GetDependency<IDeleteProjectCommand>().Received(1)
|
await sutProvider.GetDependency<IDeleteProjectCommand>().Received(1)
|
||||||
.DeleteProjects(Arg.Is(ids));
|
.DeleteProjects(Arg.Is(ids), Arg.Any<Guid>());
|
||||||
Assert.Equal(data.Count, results.Data.Count());
|
Assert.Equal(data.Count, results.Data.Count());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,6 +39,7 @@ public class ProjectsControllerTests
|
|||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async void BulkDeleteProjects_NoGuids_ThrowsArgumentNullException(SutProvider<ProjectsController> sutProvider)
|
public async void BulkDeleteProjects_NoGuids_ThrowsArgumentNullException(SutProvider<ProjectsController> sutProvider)
|
||||||
{
|
{
|
||||||
|
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
|
||||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.BulkDeleteProjectsAsync(new List<Guid>()));
|
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.BulkDeleteProjectsAsync(new List<Guid>()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user