1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-25 12:45:18 +01:00

SM-695: Block Create & Update for Admins on Secrets Outside of the Org (#2844)

* SM-695: Block create or update for admins on secrets outside of the org

* SM-695: Update test, org is required on project

* SM-695: Update tests to set matching org id in project

* SM-695: Ensure there is no more than 1 project connected to a secret, plus remove org admin check in the CreateSecretCommand.

* SM-695: Add integration tests for create and update secrets security fixes

* SM-695: Update Create and Update secret tests, a secret can only be in one project at a time
This commit is contained in:
Colton Hurst 2023-04-14 09:48:11 -04:00 committed by GitHub
parent f5a8cf5c9c
commit 2529c5b36f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 147 additions and 1 deletions

View File

@ -31,6 +31,11 @@ public class CreateSecretCommand : ICreateSecretCommand
throw new NotFoundException(); throw new NotFoundException();
} }
if (secret.Projects != null && secret.Projects.Any() && !(await _projectRepository.ProjectsAreInOrganization(secret.Projects.Select(p => p.Id).ToList(), secret.OrganizationId)))
{
throw new NotFoundException();
}
var hasAccess = accessClient switch var hasAccess = accessClient switch
{ {
AccessClientType.NoAccessCheck => true, AccessClientType.NoAccessCheck => true,

View File

@ -28,6 +28,11 @@ public class UpdateSecretCommand : IUpdateSecretCommand
throw new NotFoundException(); throw new NotFoundException();
} }
if (updatedSecret.Projects != null && updatedSecret.Projects.Any() && !(await _projectRepository.ProjectsAreInOrganization(updatedSecret.Projects.Select(p => p.Id).ToList(), secret.OrganizationId)))
{
throw new NotFoundException();
}
var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId); var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin); var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);

View File

@ -190,4 +190,13 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
return (policy.Read, policy.Write); return (policy.Read, policy.Write);
} }
public async Task<bool> ProjectsAreInOrganization(List<Guid> projectIds, Guid organizationId)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var results = await dbContext.Project.Where(p => p.OrganizationId == organizationId && projectIds.Contains(p.Id)).ToListAsync();
return projectIds.Count == results.Count;
}
} }

View File

@ -22,6 +22,9 @@ public class CreateSecretCommandTests
public async Task CreateAsync_Success(PermissionType permissionType, Secret data, public async Task CreateAsync_Success(PermissionType permissionType, Secret data,
SutProvider<CreateSecretCommand> sutProvider, Guid userId, Project mockProject) SutProvider<CreateSecretCommand> sutProvider, Guid userId, Project mockProject)
{ {
sutProvider.GetDependency<IProjectRepository>().ProjectsAreInOrganization(default, default).ReturnsForAnyArgs(true);
mockProject.OrganizationId = data.OrganizationId;
data.Projects = new List<Project>() { mockProject }; data.Projects = new List<Project>() { mockProject };
if (permissionType == PermissionType.RunAsAdmin) if (permissionType == PermissionType.RunAsAdmin)

View File

@ -34,6 +34,9 @@ public class UpdateSecretCommandTests
public async Task UpdateAsync_Success(PermissionType permissionType, Secret data, SutProvider<UpdateSecretCommand> sutProvider, Guid userId, Project mockProject) public async Task UpdateAsync_Success(PermissionType permissionType, Secret data, SutProvider<UpdateSecretCommand> sutProvider, Guid userId, Project mockProject)
{ {
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(data.OrganizationId).Returns(true); sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(data.OrganizationId).Returns(true);
sutProvider.GetDependency<IProjectRepository>().ProjectsAreInOrganization(default, default).ReturnsForAnyArgs(true);
mockProject.OrganizationId = data.OrganizationId;
data.Projects = new List<Project>() { mockProject }; data.Projects = new List<Project>() { mockProject };
if (permissionType == PermissionType.RunAsAdmin) if (permissionType == PermissionType.RunAsAdmin)

View File

@ -80,6 +80,11 @@ public class SecretsController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
if (createRequest.ProjectIds != null && createRequest.ProjectIds.Length > 1)
{
throw new BadRequestException();
}
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var result = await _createSecretCommand.CreateAsync(createRequest.ToSecret(organizationId), userId); var result = await _createSecretCommand.CreateAsync(createRequest.ToSecret(organizationId), userId);
@ -140,6 +145,11 @@ public class SecretsController : Controller
[HttpPut("secrets/{id}")] [HttpPut("secrets/{id}")]
public async Task<SecretResponseModel> UpdateSecretAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest) public async Task<SecretResponseModel> UpdateSecretAsync([FromRoute] Guid id, [FromBody] SecretUpdateRequestModel updateRequest)
{ {
if (updateRequest.ProjectIds != null && updateRequest.ProjectIds.Length > 1)
{
throw new BadRequestException();
}
var userId = _userService.GetProperUserId(User).Value; var userId = _userService.GetProperUserId(User).Value;
var secret = updateRequest.ToSecret(id); var secret = updateRequest.ToSecret(id);
var result = await _updateSecretCommand.UpdateAsync(secret, userId); var result = await _updateSecretCommand.UpdateAsync(secret, userId);

View File

@ -18,4 +18,5 @@ public interface IProjectRepository
Task<bool> ServiceAccountHasWriteAccessToProject(Guid id, Guid userId); Task<bool> ServiceAccountHasWriteAccessToProject(Guid id, Guid userId);
Task<bool> ServiceAccountHasReadAccessToProject(Guid id, Guid userId); Task<bool> ServiceAccountHasReadAccessToProject(Guid id, Guid userId);
Task<(bool Read, bool Write)> AccessToProjectAsync(Guid id, Guid userId, AccessClientType accessType); Task<(bool Read, bool Write)> AccessToProjectAsync(Guid id, Guid userId, AccessClientType accessType);
Task<bool> ProjectsAreInOrganization(List<Guid> projectIds, Guid organizationId);
} }

View File

@ -148,7 +148,7 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
var (org, _) = await _organizationHelper.Initialize(true, true); var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email); await LoginAsync(_email);
var project = await _projectRepository.CreateAsync(new Project { Name = "123" }); var project = await _projectRepository.CreateAsync(new Project { OrganizationId = org.Id, Name = "123" });
var request = new SecretCreateRequestModel var request = new SecretCreateRequestModel
{ {
@ -179,6 +179,47 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
Assert.Null(createdSecret.DeletedDate); Assert.Null(createdSecret.DeletedDate);
} }
[Fact]
public async Task CreateWithDifferentProjectOrgId_RunAsAdmin_NotFound()
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var project = await _projectRepository.CreateAsync(new Project { Name = "123" });
var request = new SecretCreateRequestModel
{
ProjectIds = new Guid[] { project.Id },
Key = _mockEncryptedString,
Value = _mockEncryptedString,
Note = _mockEncryptedString,
};
var response = await _client.PostAsJsonAsync($"/organizations/{org.Id}/secrets", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task CreateWithMultipleProjects_RunAsAdmin_BadRequest()
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var projectA = await _projectRepository.CreateAsync(new Project { OrganizationId = org.Id, Name = "123A" });
var projectB = await _projectRepository.CreateAsync(new Project { OrganizationId = org.Id, Name = "123B" });
var request = new SecretCreateRequestModel
{
ProjectIds = new Guid[] { projectA.Id, projectB.Id },
Key = _mockEncryptedString,
Value = _mockEncryptedString,
Note = _mockEncryptedString,
};
var response = await _client.PostAsJsonAsync($"/organizations/{org.Id}/secrets", request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact] [Fact]
public async Task CreateWithoutProject_RunAsUser_NotFound() public async Task CreateWithoutProject_RunAsUser_NotFound()
{ {
@ -531,6 +572,63 @@ public class SecretsControllerTests : IClassFixture<ApiApplicationFactory>, IAsy
Assert.NotEqual(secret.RevisionDate, updatedSecret.RevisionDate); Assert.NotEqual(secret.RevisionDate, updatedSecret.RevisionDate);
} }
[Fact]
public async Task UpdateWithDifferentProjectOrgId_RunAsAdmin_NotFound()
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var project = await _projectRepository.CreateAsync(new Project { Name = "123" });
var secret = await _secretRepository.CreateAsync(new Secret
{
OrganizationId = org.Id,
Key = _mockEncryptedString,
Value = _mockEncryptedString,
Note = _mockEncryptedString
});
var request = new SecretUpdateRequestModel
{
Key = _mockEncryptedString,
Value = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=",
Note = _mockEncryptedString,
ProjectIds = new Guid[] { project.Id },
};
var response = await _client.PutAsJsonAsync($"/secrets/{secret.Id}", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task UpdateWithMultipleProjects_BadRequest()
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var projectA = await _projectRepository.CreateAsync(new Project { OrganizationId = org.Id, Name = "123A" });
var projectB = await _projectRepository.CreateAsync(new Project { OrganizationId = org.Id, Name = "123B" });
var secret = await _secretRepository.CreateAsync(new Secret
{
OrganizationId = org.Id,
Key = _mockEncryptedString,
Value = _mockEncryptedString,
Note = _mockEncryptedString
});
var request = new SecretUpdateRequestModel
{
Key = _mockEncryptedString,
Value = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=",
Note = _mockEncryptedString,
ProjectIds = new Guid[] { projectA.Id, projectB.Id },
};
var response = await _client.PutAsJsonAsync($"/secrets/{secret.Id}", request);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Theory] [Theory]
[InlineData(false, false)] [InlineData(false, false)]
[InlineData(true, false)] [InlineData(true, false)]

View File

@ -125,6 +125,12 @@ public class SecretsControllerTests
[BitAutoData(PermissionType.RunAsUserWithPermission)] [BitAutoData(PermissionType.RunAsUserWithPermission)]
public async void CreateSecret_Success(PermissionType permissionType, SutProvider<SecretsController> sutProvider, SecretCreateRequestModel data, Guid organizationId, Project mockProject, Guid userId) public async void CreateSecret_Success(PermissionType permissionType, SutProvider<SecretsController> sutProvider, SecretCreateRequestModel data, Guid organizationId, Project mockProject, Guid userId)
{ {
// We currently only allow a secret to be in one project at a time
if (data.ProjectIds != null && data.ProjectIds.Length > 1)
{
data.ProjectIds = new Guid[] { data.ProjectIds.ElementAt(0) };
}
var resultSecret = data.ToSecret(organizationId); var resultSecret = data.ToSecret(organizationId);
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
@ -152,6 +158,12 @@ public class SecretsControllerTests
[BitAutoData(PermissionType.RunAsUserWithPermission)] [BitAutoData(PermissionType.RunAsUserWithPermission)]
public async void UpdateSecret_Success(PermissionType permissionType, SutProvider<SecretsController> sutProvider, SecretUpdateRequestModel data, Guid secretId, Guid organizationId, Guid userId, Project mockProject) public async void UpdateSecret_Success(PermissionType permissionType, SutProvider<SecretsController> sutProvider, SecretUpdateRequestModel data, Guid secretId, Guid organizationId, Guid userId, Project mockProject)
{ {
// We currently only allow a secret to be in one project at a time
if (data.ProjectIds != null && data.ProjectIds.Length > 1)
{
data.ProjectIds = new Guid[] { data.ProjectIds.ElementAt(0) };
}
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
if (permissionType == PermissionType.RunAsAdmin) if (permissionType == PermissionType.RunAsAdmin)