1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36:14 -05:00

[SM-704] Extract Authorization For ServiceAccounts (#2869)

* Move to access query for project commands

* Swap to hasAccess method per action

* Swap to authorization handler pattern

* Move ProjectOperationRequirement to Core

* Add default throw + tests

* Extract authorization out of commands

* Unit tests for authorization handler

* Formatting

* Swap to reflection for testing switch

* Swap to check read & reflections in test

* fix wording on exception

* Refactor GetAccessClient into its own query

* Use accessClientQuery in project handler
This commit is contained in:
Thomas Avery
2023-05-31 13:49:58 -05:00
committed by GitHub
parent c08e2a7473
commit d1155ee376
16 changed files with 694 additions and 249 deletions

View File

@ -139,11 +139,21 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetByServiceAccountId_ServiceAccountDoesNotExist_NotFound()
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var response = await _client.GetAsync($"/service-accounts/{new Guid()}");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetByServiceAccountId_UserWithoutPermission_NotFound()
{
var (org, _) = await _organizationHelper.Initialize(true, true);
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
@ -161,30 +171,7 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task GetByServiceAccountId_Success(PermissionType permissionType)
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
if (permissionType == PermissionType.RunAsUserWithPermission)
{
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> {
new UserServiceAccountAccessPolicy
{
GrantedServiceAccountId = serviceAccount.Id,
OrganizationUserId = orgUser.Id,
Write = true,
Read = true,
},
});
}
var serviceAccount = await SetupServiceAccountWithAccessAsync(permissionType);
var response = await _client.GetAsync($"/service-accounts/{serviceAccount.Id}");
response.EnsureSuccessStatusCode();
@ -212,12 +199,25 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task Create_Admin_Success()
[Theory]
[InlineData(PermissionType.RunAsAdmin)]
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task Create_Success(PermissionType permissionType)
{
var (org, orgUser) = await _organizationHelper.Initialize(true, true);
var (org, adminOrgUser) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var orgUserId = adminOrgUser.Id;
var currentUserId = adminOrgUser.UserId!.Value;
if (permissionType == PermissionType.RunAsUserWithPermission)
{
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
orgUserId = orgUser.Id;
currentUserId = orgUser.UserId!.Value;
}
var request = new ServiceAccountCreateRequestModel { Name = _mockEncryptedString };
var response = await _client.PostAsJsonAsync($"/organizations/{org.Id}/service-accounts", request);
@ -236,9 +236,11 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
AssertHelper.AssertRecent(createdServiceAccount.CreationDate);
// Check permissions have been bootstrapped.
var accessPolicies = await _accessPolicyRepository.GetManyByGrantedServiceAccountIdAsync(createdServiceAccount.Id, orgUser.UserId!.Value);
var accessPolicies = await _accessPolicyRepository.GetManyByGrantedServiceAccountIdAsync(createdServiceAccount.Id, currentUserId);
Assert.NotNull(accessPolicies);
var ap = accessPolicies!.First();
var ap = (UserServiceAccountAccessPolicy)accessPolicies.First();
Assert.Equal(createdServiceAccount.Id, ap.GrantedServiceAccountId);
Assert.Equal(orgUserId, ap.OrganizationUserId);
Assert.True(ap.Read);
Assert.True(ap.Write);
AssertHelper.AssertRecent(ap.CreationDate);
@ -266,73 +268,6 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task Update_Admin()
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };
var response = await _client.PutAsJsonAsync($"/service-accounts/{initialServiceAccount.Id}", request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ServiceAccountResponseModel>();
Assert.NotNull(result);
Assert.Equal(request.Name, result!.Name);
Assert.NotEqual(initialServiceAccount.Name, result.Name);
AssertHelper.AssertRecent(result.RevisionDate);
Assert.NotEqual(initialServiceAccount.RevisionDate, result.RevisionDate);
var updatedServiceAccount = await _serviceAccountRepository.GetByIdAsync(initialServiceAccount.Id);
Assert.NotNull(result);
Assert.Equal(request.Name, updatedServiceAccount.Name);
AssertHelper.AssertRecent(updatedServiceAccount.RevisionDate);
AssertHelper.AssertRecent(updatedServiceAccount.CreationDate);
Assert.NotEqual(initialServiceAccount.Name, updatedServiceAccount.Name);
Assert.NotEqual(initialServiceAccount.RevisionDate, updatedServiceAccount.RevisionDate);
}
[Fact]
public async Task Update_User_WithPermission()
{
var (org, _) = await _organizationHelper.Initialize(true, true);
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
await CreateUserPolicyAsync(orgUser.Id, initialServiceAccount.Id, true, true);
var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };
var response = await _client.PutAsJsonAsync($"/service-accounts/{initialServiceAccount.Id}", request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ServiceAccountResponseModel>();
Assert.NotNull(result);
Assert.Equal(request.Name, result!.Name);
Assert.NotEqual(initialServiceAccount.Name, result.Name);
AssertHelper.AssertRecent(result.RevisionDate);
Assert.NotEqual(initialServiceAccount.RevisionDate, result.RevisionDate);
var updatedServiceAccount = await _serviceAccountRepository.GetByIdAsync(initialServiceAccount.Id);
Assert.NotNull(result);
Assert.Equal(request.Name, updatedServiceAccount.Name);
AssertHelper.AssertRecent(updatedServiceAccount.RevisionDate);
AssertHelper.AssertRecent(updatedServiceAccount.CreationDate);
Assert.NotEqual(initialServiceAccount.Name, updatedServiceAccount.Name);
Assert.NotEqual(initialServiceAccount.RevisionDate, updatedServiceAccount.RevisionDate);
}
[Fact]
public async Task Update_User_NoPermissions()
{
@ -352,6 +287,45 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task Update_NonExistingServiceAccount_NotFound()
{
await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };
var response = await _client.PutAsJsonAsync("/service-accounts/c53de509-4581-402c-8cbd-f26d2c516fba", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Theory]
[InlineData(PermissionType.RunAsAdmin)]
[InlineData(PermissionType.RunAsUserWithPermission)]
public async Task Update_Success(PermissionType permissionType)
{
var initialServiceAccount = await SetupServiceAccountWithAccessAsync(permissionType);
var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };
var response = await _client.PutAsJsonAsync($"/service-accounts/{initialServiceAccount.Id}", request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ServiceAccountResponseModel>();
Assert.NotNull(result);
Assert.Equal(request.Name, result!.Name);
Assert.NotEqual(initialServiceAccount.Name, result.Name);
AssertHelper.AssertRecent(result.RevisionDate);
Assert.NotEqual(initialServiceAccount.RevisionDate, result.RevisionDate);
var updatedServiceAccount = await _serviceAccountRepository.GetByIdAsync(initialServiceAccount.Id);
Assert.NotNull(result);
Assert.Equal(request.Name, updatedServiceAccount.Name);
AssertHelper.AssertRecent(updatedServiceAccount.RevisionDate);
AssertHelper.AssertRecent(updatedServiceAccount.CreationDate);
Assert.NotEqual(initialServiceAccount.Name, updatedServiceAccount.Name);
Assert.NotEqual(initialServiceAccount.RevisionDate, updatedServiceAccount.RevisionDate);
}
[Theory]
[InlineData(false, false)]
[InlineData(true, false)]
@ -837,4 +811,35 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
return serviceAccountIds;
}
private async Task<ServiceAccount> SetupServiceAccountWithAccessAsync(PermissionType permissionType)
{
var (org, _) = await _organizationHelper.Initialize(true, true);
await LoginAsync(_email);
var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = org.Id,
Name = _mockEncryptedString,
});
if (permissionType == PermissionType.RunAsAdmin)
{
return initialServiceAccount;
}
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
await LoginAsync(email);
var accessPolicies = new List<BaseAccessPolicy>
{
new UserServiceAccountAccessPolicy
{
GrantedServiceAccountId = initialServiceAccount.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true,
},
};
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
return initialServiceAccount;
}
}

View File

@ -1,4 +1,5 @@
using Bit.Api.SecretsManager.Controllers;
using System.Security.Claims;
using Bit.Api.SecretsManager.Controllers;
using Bit.Api.SecretsManager.Models.Request;
using Bit.Core.Context;
using Bit.Core.Enums;
@ -11,6 +12,7 @@ using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
@ -62,13 +64,30 @@ public class ServiceAccountsControllerTests
sutProvider.Sut.ListByOrganizationAsync(orgId));
}
[Theory]
[BitAutoData]
public async void CreateServiceAccount_NoAccess_Throws(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountCreateRequestModel data, Guid organizationId)
{
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToServiceAccount(organizationId),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
var resultServiceAccount = data.ToServiceAccount(organizationId);
sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default, default).ReturnsForAnyArgs(resultServiceAccount);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(organizationId, data));
await sutProvider.GetDependency<ICreateServiceAccountCommand>().DidNotReceiveWithAnyArgs()
.CreateAsync(Arg.Any<ServiceAccount>(), Arg.Any<Guid>());
}
[Theory]
[BitAutoData]
public async void CreateServiceAccount_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountCreateRequestModel data, Guid organizationId)
{
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToServiceAccount(organizationId),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(organizationId).Returns(true);
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(true);
var resultServiceAccount = data.ToServiceAccount(organizationId);
sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default, default).ReturnsForAnyArgs(resultServiceAccount);
@ -79,30 +98,33 @@ public class ServiceAccountsControllerTests
[Theory]
[BitAutoData]
public async void CreateServiceAccount_NotOrgUser_Throws(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountCreateRequestModel data, Guid organizationId)
public async void UpdateServiceAccount_NoAccess_Throws(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountUpdateRequestModel data, ServiceAccount existingServiceAccount)
{
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(false);
var resultServiceAccount = data.ToServiceAccount(organizationId);
sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default, default).ReturnsForAnyArgs(resultServiceAccount);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToServiceAccount(existingServiceAccount.Id),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Failed());
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).ReturnsForAnyArgs(existingServiceAccount);
var resultServiceAccount = data.ToServiceAccount(existingServiceAccount.Id);
sutProvider.GetDependency<IUpdateServiceAccountCommand>().UpdateAsync(default).ReturnsForAnyArgs(resultServiceAccount);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateAsync(organizationId, data));
await sutProvider.GetDependency<ICreateServiceAccountCommand>()
.DidNotReceiveWithAnyArgs()
.CreateAsync(Arg.Any<ServiceAccount>(), Arg.Any<Guid>());
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(existingServiceAccount.Id, data));
await sutProvider.GetDependency<IUpdateServiceAccountCommand>().DidNotReceiveWithAnyArgs()
.UpdateAsync(Arg.Any<ServiceAccount>());
}
[Theory]
[BitAutoData]
public async void UpdateServiceAccount_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountUpdateRequestModel data, Guid serviceAccountId)
public async void UpdateServiceAccount_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountUpdateRequestModel data, ServiceAccount existingServiceAccount)
{
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
var resultServiceAccount = data.ToServiceAccount(serviceAccountId);
sutProvider.GetDependency<IUpdateServiceAccountCommand>().UpdateAsync(default, default).ReturnsForAnyArgs(resultServiceAccount);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToServiceAccount(existingServiceAccount.Id),
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
var resultServiceAccount = data.ToServiceAccount(existingServiceAccount.Id);
sutProvider.GetDependency<IUpdateServiceAccountCommand>().UpdateAsync(default).ReturnsForAnyArgs(resultServiceAccount);
var result = await sutProvider.Sut.UpdateAsync(serviceAccountId, data);
var result = await sutProvider.Sut.UpdateAsync(existingServiceAccount.Id, data);
await sutProvider.GetDependency<IUpdateServiceAccountCommand>().Received(1)
.UpdateAsync(Arg.Any<ServiceAccount>(), Arg.Any<Guid>());
.UpdateAsync(Arg.Any<ServiceAccount>());
}
[Theory]