1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

[SM-382] Service Account access policy checks (#2603)

The purpose of this PR is to add access policy checks to service account endpoints.
This commit is contained in:
Thomas Avery 2023-01-24 09:50:04 -06:00 committed by GitHub
parent bdea036c1f
commit aa9f859306
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 691 additions and 101 deletions

View File

@ -1,4 +1,7 @@
using Bit.Core.Entities;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.SecretManagerFeatures.AccessTokens.Interfaces;
using Bit.Core.Utilities;
@ -7,16 +10,45 @@ namespace Bit.Commercial.Core.SecretManagerFeatures.AccessTokens;
public class CreateAccessTokenCommand : ICreateAccessTokenCommand
{
private readonly int _clientSecretMaxLength = 30;
private readonly IApiKeyRepository _apiKeyRepository;
private readonly int _clientSecretMaxLength = 30;
private readonly ICurrentContext _currentContext;
private readonly IServiceAccountRepository _serviceAccountRepository;
public CreateAccessTokenCommand(IApiKeyRepository apiKeyRepository)
public CreateAccessTokenCommand(
IApiKeyRepository apiKeyRepository,
ICurrentContext currentContext,
IServiceAccountRepository serviceAccountRepository)
{
_apiKeyRepository = apiKeyRepository;
_currentContext = currentContext;
_serviceAccountRepository = serviceAccountRepository;
}
public async Task<ApiKey> CreateAsync(ApiKey apiKey)
public async Task<ApiKey> CreateAsync(ApiKey apiKey, Guid userId)
{
if (apiKey.ServiceAccountId == null)
{
throw new BadRequestException();
}
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(apiKey.ServiceAccountId.Value);
var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var hasAccess = accessClient switch
{
AccessClientType.NoAccessCheck => true,
AccessClientType.User => await _serviceAccountRepository.UserHasWriteAccessToServiceAccount(
apiKey.ServiceAccountId.Value, userId),
_ => false,
};
if (!hasAccess)
{
throw new UnauthorizedAccessException();
}
apiKey.ClientSecret = CoreHelpers.SecureRandomString(_clientSecretMaxLength);
return await _apiKeyRepository.CreateAsync(apiKey);
}

View File

@ -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.Repositories;
using Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces;
@ -8,22 +10,38 @@ namespace Bit.Commercial.Core.SecretManagerFeatures.ServiceAccounts;
public class UpdateServiceAccountCommand : IUpdateServiceAccountCommand
{
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly ICurrentContext _currentContext;
public UpdateServiceAccountCommand(IServiceAccountRepository serviceAccountRepository)
public UpdateServiceAccountCommand(IServiceAccountRepository serviceAccountRepository, ICurrentContext currentContext)
{
_serviceAccountRepository = serviceAccountRepository;
_currentContext = currentContext;
}
public async Task<ServiceAccount> UpdateAsync(ServiceAccount serviceAccount)
public async Task<ServiceAccount> UpdateAsync(ServiceAccount updatedServiceAccount, Guid userId)
{
var existingServiceAccount = await _serviceAccountRepository.GetByIdAsync(serviceAccount.Id);
if (existingServiceAccount == null)
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(updatedServiceAccount.Id);
if (serviceAccount == null)
{
throw new NotFoundException();
}
serviceAccount.OrganizationId = existingServiceAccount.OrganizationId;
serviceAccount.CreationDate = existingServiceAccount.CreationDate;
var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var hasAccess = accessClient switch
{
AccessClientType.NoAccessCheck => true,
AccessClientType.User => await _serviceAccountRepository.UserHasWriteAccessToServiceAccount(updatedServiceAccount.Id, userId),
_ => false,
};
if (!hasAccess)
{
throw new UnauthorizedAccessException();
}
serviceAccount.Name = updatedServiceAccount.Name;
serviceAccount.RevisionDate = DateTime.UtcNow;
await _serviceAccountRepository.ReplaceAsync(serviceAccount);

View File

@ -31,12 +31,25 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
await dbContext.AddAsync(entity);
break;
}
case Core.Entities.UserServiceAccountAccessPolicy accessPolicy:
{
var entity =
Mapper.Map<UserServiceAccountAccessPolicy>(accessPolicy);
await dbContext.AddAsync(entity);
break;
}
case Core.Entities.GroupProjectAccessPolicy accessPolicy:
{
var entity = Mapper.Map<GroupProjectAccessPolicy>(accessPolicy);
await dbContext.AddAsync(entity);
break;
}
case Core.Entities.GroupServiceAccountAccessPolicy accessPolicy:
{
var entity = Mapper.Map<GroupServiceAccountAccessPolicy>(accessPolicy);
await dbContext.AddAsync(entity);
break;
}
case Core.Entities.ServiceAccountProjectAccessPolicy accessPolicy:
{
var entity = Mapper.Map<ServiceAccountProjectAccessPolicy>(accessPolicy);

View File

@ -1,4 +1,6 @@
using AutoMapper;
using System.Linq.Expressions;
using AutoMapper;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Infrastructure.EntityFramework.Models;
using Bit.Infrastructure.EntityFramework.Repositories;
@ -13,16 +15,50 @@ public class ServiceAccountRepository : Repository<Core.Entities.ServiceAccount,
: base(serviceScopeFactory, mapper, db => db.ServiceAccount)
{ }
public async Task<IEnumerable<Core.Entities.ServiceAccount>> GetManyByOrganizationIdAsync(Guid organizationId)
public async Task<IEnumerable<Core.Entities.ServiceAccount>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
{
using (var scope = ServiceScopeFactory.CreateScope())
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = dbContext.ServiceAccount.Where(c => c.OrganizationId == organizationId);
query = accessType switch
{
var dbContext = GetDatabaseContext(scope);
var serviceAccounts = await dbContext.ServiceAccount
.Where(c => c.OrganizationId == organizationId)
.OrderBy(c => c.RevisionDate)
.ToListAsync();
return Mapper.Map<List<Core.Entities.ServiceAccount>>(serviceAccounts);
}
AccessClientType.NoAccessCheck => query,
AccessClientType.User => query.Where(UserHasReadAccessToServiceAccount(userId)),
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),
};
var serviceAccounts = await query.OrderBy(c => c.RevisionDate).ToListAsync();
return Mapper.Map<List<Core.Entities.ServiceAccount>>(serviceAccounts);
}
public async Task<bool> UserHasReadAccessToServiceAccount(Guid id, Guid userId)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = dbContext.ServiceAccount
.Where(sa => sa.Id == id)
.Where(UserHasReadAccessToServiceAccount(userId));
return await query.AnyAsync();
}
public async Task<bool> UserHasWriteAccessToServiceAccount(Guid id, Guid userId)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = dbContext.ServiceAccount
.Where(sa => sa.Id == id)
.Where(UserHasWriteAccessToServiceAccount(userId));
return await query.AnyAsync();
}
private static Expression<Func<ServiceAccount, bool>> UserHasReadAccessToServiceAccount(Guid userId) => sa =>
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read));
private static Expression<Func<ServiceAccount, bool>> UserHasWriteAccessToServiceAccount(Guid userId) => sa =>
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write));
}

View File

@ -1,5 +1,7 @@
using Bit.Commercial.Core.SecretManagerFeatures.AccessTokens;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@ -14,10 +16,57 @@ public class CreateServiceAccountCommandTests
{
[Theory]
[BitAutoData]
public async Task CreateAsync_CallsCreate(ApiKey data,
SutProvider<CreateAccessTokenCommand> sutProvider)
public async Task CreateAsync_NoServiceAccountId_ThrowsBadRequestException(ApiKey data, Guid userId,
SutProvider<CreateAccessTokenCommand> sutProvider)
{
await sutProvider.Sut.CreateAsync(data);
data.ServiceAccountId = null;
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAsync(data, userId));
await sutProvider.GetDependency<IApiKeyRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default);
}
[Theory]
[BitAutoData]
public async Task CreateAsync_User_NoAccess(ApiKey data, Guid userId, ServiceAccount saData,
SutProvider<CreateAccessTokenCommand> sutProvider)
{
data.ServiceAccountId = saData.Id;
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(saData.Id).Returns(saData);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(saData.Id, userId).Returns(false);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.CreateAsync(data, userId));
await sutProvider.GetDependency<IApiKeyRepository>().DidNotReceiveWithAnyArgs().CreateAsync(default);
}
[Theory]
[BitAutoData]
public async Task CreateAsync_User_Success(ApiKey data, Guid userId, ServiceAccount saData,
SutProvider<CreateAccessTokenCommand> sutProvider)
{
data.ServiceAccountId = saData.Id;
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(saData.Id).Returns(saData);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(saData.Id, userId).Returns(true);
await sutProvider.Sut.CreateAsync(data, userId);
await sutProvider.GetDependency<IApiKeyRepository>().Received(1)
.CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
}
[Theory]
[BitAutoData]
public async Task CreateAsync_Admin_Succeeds(ApiKey data, Guid userId, ServiceAccount saData,
SutProvider<CreateAccessTokenCommand> sutProvider)
{
data.ServiceAccountId = saData.Id;
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(saData.Id).Returns(saData);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(saData.OrganizationId).Returns(true);
await sutProvider.Sut.CreateAsync(data, userId);
await sutProvider.GetDependency<IApiKeyRepository>().Received(1)
.CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));

View File

@ -1,4 +1,5 @@
using Bit.Commercial.Core.SecretManagerFeatures.ServiceAccounts;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
@ -15,19 +16,47 @@ public class UpdateServiceAccountCommandTests
{
[Theory]
[BitAutoData]
public async Task UpdateAsync_ServiceAccountDoesNotExist_ThrowsNotFound(ServiceAccount data, SutProvider<UpdateServiceAccountCommand> sutProvider)
public async Task UpdateAsync_ServiceAccountDoesNotExist_ThrowsNotFound(ServiceAccount data, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data));
var exception = await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.UpdateAsync(data, userId));
await sutProvider.GetDependency<IServiceAccountRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
[Theory]
[BitAutoData]
public async Task UpdateAsync_CallsReplaceAsync(ServiceAccount data, SutProvider<UpdateServiceAccountCommand> sutProvider)
public async Task UpdateAsync_User_NoAccess(ServiceAccount data, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);
await sutProvider.Sut.UpdateAsync(data);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(data.Id, userId).Returns(false);
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.UpdateAsync(data, userId));
await sutProvider.GetDependency<IServiceAccountRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
}
[Theory]
[BitAutoData]
public async Task UpdateAsync_User_Success(ServiceAccount data, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(data.Id, userId).Returns(true);
await sutProvider.Sut.UpdateAsync(data, userId);
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)
.ReplaceAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
}
[Theory]
[BitAutoData]
public async Task UpdateAsync_Admin_Success(ServiceAccount data, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(data.Id).Returns(data);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(true);
await sutProvider.Sut.UpdateAsync(data, userId);
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)
.ReplaceAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
@ -35,9 +64,10 @@ public class UpdateServiceAccountCommandTests
[Theory]
[BitAutoData]
public async Task UpdateAsync_DoesNotModifyOrganizationId(ServiceAccount existingServiceAccount, SutProvider<UpdateServiceAccountCommand> sutProvider)
public async Task UpdateAsync_DoesNotModifyOrganizationId(ServiceAccount existingServiceAccount, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);
var updatedOrgId = Guid.NewGuid();
var serviceAccountUpdate = new ServiceAccount()
@ -47,7 +77,7 @@ public class UpdateServiceAccountCommandTests
Name = existingServiceAccount.Name,
};
var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate);
var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate, userId);
Assert.Equal(existingServiceAccount.OrganizationId, result.OrganizationId);
Assert.NotEqual(existingServiceAccount.OrganizationId, updatedOrgId);
@ -55,9 +85,10 @@ public class UpdateServiceAccountCommandTests
[Theory]
[BitAutoData]
public async Task UpdateAsync_DoesNotModifyCreationDate(ServiceAccount existingServiceAccount, SutProvider<UpdateServiceAccountCommand> sutProvider)
public async Task UpdateAsync_DoesNotModifyCreationDate(ServiceAccount existingServiceAccount, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);
var updatedCreationDate = DateTime.UtcNow;
var serviceAccountUpdate = new ServiceAccount()
@ -67,7 +98,7 @@ public class UpdateServiceAccountCommandTests
Name = existingServiceAccount.Name,
};
var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate);
var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate, userId);
Assert.Equal(existingServiceAccount.CreationDate, result.CreationDate);
Assert.NotEqual(existingServiceAccount.CreationDate, updatedCreationDate);
@ -75,9 +106,10 @@ public class UpdateServiceAccountCommandTests
[Theory]
[BitAutoData]
public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(ServiceAccount existingServiceAccount, SutProvider<UpdateServiceAccountCommand> sutProvider)
public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(ServiceAccount existingServiceAccount, Guid userId, SutProvider<UpdateServiceAccountCommand> sutProvider)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);
var updatedRevisionDate = DateTime.UtcNow.AddDays(10);
var serviceAccountUpdate = new ServiceAccount()
@ -87,9 +119,9 @@ public class UpdateServiceAccountCommandTests
Name = existingServiceAccount.Name,
};
var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate);
var result = await sutProvider.Sut.UpdateAsync(serviceAccountUpdate, userId);
Assert.NotEqual(existingServiceAccount.RevisionDate, result.RevisionDate);
Assert.NotEqual(serviceAccountUpdate.RevisionDate, result.RevisionDate);
AssertHelper.AssertRecent(result.RevisionDate);
}
}

View File

@ -3,9 +3,13 @@ using Bit.Api.Models.Response.SecretsManager;
using Bit.Api.SecretManagerFeatures.Models.Request;
using Bit.Api.SecretManagerFeatures.Models.Response;
using Bit.Api.Utilities;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.SecretManagerFeatures.AccessTokens.Interfaces;
using Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces;
using Bit.Core.Services;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Controllers;
@ -14,59 +18,105 @@ namespace Bit.Api.Controllers;
[Route("service-accounts")]
public class ServiceAccountsController : Controller
{
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IApiKeyRepository _apiKeyRepository;
private readonly ICreateServiceAccountCommand _createServiceAccountCommand;
private readonly ICreateAccessTokenCommand _createAccessTokenCommand;
private readonly ICreateServiceAccountCommand _createServiceAccountCommand;
private readonly ICurrentContext _currentContext;
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand;
private readonly IUserService _userService;
public ServiceAccountsController(
IUserService userService,
IServiceAccountRepository serviceAccountRepository,
ICreateAccessTokenCommand createAccessTokenCommand,
IApiKeyRepository apiKeyRepository, ICreateServiceAccountCommand createServiceAccountCommand,
IUpdateServiceAccountCommand updateServiceAccountCommand)
IUpdateServiceAccountCommand updateServiceAccountCommand,
ICurrentContext currentContext)
{
_userService = userService;
_serviceAccountRepository = serviceAccountRepository;
_apiKeyRepository = apiKeyRepository;
_createServiceAccountCommand = createServiceAccountCommand;
_updateServiceAccountCommand = updateServiceAccountCommand;
_createAccessTokenCommand = createAccessTokenCommand;
_currentContext = currentContext;
}
[HttpGet("/organizations/{organizationId}/service-accounts")]
public async Task<ListResponseModel<ServiceAccountResponseModel>> GetServiceAccountsByOrganizationAsync([FromRoute] Guid organizationId)
public async Task<ListResponseModel<ServiceAccountResponseModel>> GetServiceAccountsByOrganizationAsync(
[FromRoute] Guid organizationId)
{
var serviceAccounts = await _serviceAccountRepository.GetManyByOrganizationIdAsync(organizationId);
var userId = _userService.GetProperUserId(User).Value;
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var serviceAccounts =
await _serviceAccountRepository.GetManyByOrganizationIdAsync(organizationId, userId, accessClient);
var responses = serviceAccounts.Select(serviceAccount => new ServiceAccountResponseModel(serviceAccount));
return new ListResponseModel<ServiceAccountResponseModel>(responses);
}
[HttpPost("/organizations/{organizationId}/service-accounts")]
public async Task<ServiceAccountResponseModel> CreateServiceAccountAsync([FromRoute] Guid organizationId, [FromBody] ServiceAccountCreateRequestModel createRequest)
public async Task<ServiceAccountResponseModel> CreateServiceAccountAsync([FromRoute] Guid organizationId,
[FromBody] ServiceAccountCreateRequestModel createRequest)
{
if (!await _currentContext.OrganizationUser(organizationId))
{
throw new NotFoundException();
}
var result = await _createServiceAccountCommand.CreateAsync(createRequest.ToServiceAccount(organizationId));
return new ServiceAccountResponseModel(result);
}
[HttpPut("{id}")]
public async Task<ServiceAccountResponseModel> UpdateServiceAccountAsync([FromRoute] Guid id, [FromBody] ServiceAccountUpdateRequestModel updateRequest)
public async Task<ServiceAccountResponseModel> UpdateServiceAccountAsync([FromRoute] Guid id,
[FromBody] ServiceAccountUpdateRequestModel updateRequest)
{
var result = await _updateServiceAccountCommand.UpdateAsync(updateRequest.ToServiceAccount(id));
var userId = _userService.GetProperUserId(User).Value;
var result = await _updateServiceAccountCommand.UpdateAsync(updateRequest.ToServiceAccount(id), userId);
return new ServiceAccountResponseModel(result);
}
[HttpGet("{id}/access-tokens")]
public async Task<ListResponseModel<AccessTokenResponseModel>> GetAccessTokens([FromRoute] Guid id)
{
var userId = _userService.GetProperUserId(User).Value;
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(id);
if (serviceAccount == null)
{
throw new NotFoundException();
}
var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);
var hasAccess = accessClient switch
{
AccessClientType.NoAccessCheck => true,
AccessClientType.User => await _serviceAccountRepository.UserHasReadAccessToServiceAccount(id, userId),
_ => false,
};
if (!hasAccess)
{
throw new NotFoundException();
}
var accessTokens = await _apiKeyRepository.GetManyByServiceAccountIdAsync(id);
var responses = accessTokens.Select(token => new AccessTokenResponseModel(token));
return new ListResponseModel<AccessTokenResponseModel>(responses);
}
[HttpPost("{id}/access-tokens")]
public async Task<AccessTokenCreationResponseModel> CreateAccessTokenAsync([FromRoute] Guid id, [FromBody] AccessTokenCreateRequestModel request)
public async Task<AccessTokenCreationResponseModel> CreateAccessTokenAsync([FromRoute] Guid id,
[FromBody] AccessTokenCreateRequestModel request)
{
var result = await _createAccessTokenCommand.CreateAsync(request.ToApiKey(id));
var userId = _userService.GetProperUserId(User).Value;
var result = await _createAccessTokenCommand.CreateAsync(request.ToApiKey(id), userId);
return new AccessTokenCreationResponseModel(result);
}
}

View File

@ -17,11 +17,11 @@ public class AccessTokenResponseModel : ResponseModel
RevisionDate = apiKey.RevisionDate;
}
public Guid Id { get; }
public string Name { get; }
public ICollection<string> Scopes { get; }
public Guid Id { get; set; }
public string Name { get; set; }
public ICollection<string> Scopes { get; set; }
public DateTime? ExpireAt { get; }
public DateTime CreationDate { get; }
public DateTime RevisionDate { get; }
public DateTime? ExpireAt { get; set; }
public DateTime CreationDate { get; set; }
public DateTime RevisionDate { get; set; }
}

View File

@ -35,4 +35,3 @@ public class ServiceAccountResponseModel : ResponseModel
public DateTime RevisionDate { get; set; }
}

View File

@ -1,11 +1,14 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
namespace Bit.Core.Repositories;
public interface IServiceAccountRepository
{
Task<IEnumerable<ServiceAccount>> GetManyByOrganizationIdAsync(Guid organizationId);
Task<IEnumerable<ServiceAccount>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);
Task<ServiceAccount> GetByIdAsync(Guid id);
Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount);
Task ReplaceAsync(ServiceAccount serviceAccount);
Task<bool> UserHasReadAccessToServiceAccount(Guid id, Guid userId);
Task<bool> UserHasWriteAccessToServiceAccount(Guid id, Guid userId);
}

View File

@ -4,5 +4,5 @@ namespace Bit.Core.SecretManagerFeatures.AccessTokens.Interfaces;
public interface ICreateAccessTokenCommand
{
Task<ApiKey> CreateAsync(ApiKey apiKey);
Task<ApiKey> CreateAsync(ApiKey apiKey, Guid userId);
}

View File

@ -4,5 +4,5 @@ namespace Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces;
public interface IUpdateServiceAccountCommand
{
Task<ServiceAccount> UpdateAsync(ServiceAccount serviceAccount);
Task<ServiceAccount> UpdateAsync(ServiceAccount serviceAccount, Guid userId);
}

View File

@ -13,7 +13,10 @@ public class AccessPolicyMapperProfile : Profile
{
CreateMap<Core.Entities.UserProjectAccessPolicy, UserProjectAccessPolicy>().ReverseMap()
.ForMember(dst => dst.User, opt => opt.MapFrom(src => src.OrganizationUser.User));
CreateMap<Core.Entities.UserServiceAccountAccessPolicy, UserServiceAccountAccessPolicy>().ReverseMap()
.ForMember(dst => dst.User, opt => opt.MapFrom(src => src.OrganizationUser.User));
CreateMap<Core.Entities.GroupProjectAccessPolicy, GroupProjectAccessPolicy>().ReverseMap();
CreateMap<Core.Entities.GroupServiceAccountAccessPolicy, GroupServiceAccountAccessPolicy>().ReverseMap();
CreateMap<Core.Entities.ServiceAccountProjectAccessPolicy, ServiceAccountProjectAccessPolicy>().ReverseMap();
}
}

View File

@ -5,6 +5,8 @@ namespace Bit.Infrastructure.EntityFramework.Models;
public class ServiceAccount : Core.Entities.ServiceAccount
{
public virtual Organization Organization { get; set; }
public virtual ICollection<GroupServiceAccountAccessPolicy> GroupAccessPolicies { get; set; }
public virtual ICollection<UserServiceAccountAccessPolicy> UserAccessPolicies { get; set; }
}
public class ServiceAccountMapperProfile : Profile

View File

@ -215,7 +215,6 @@ public class ProjectsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
response.EnsureSuccessStatusCode();
var results = await response.Content.ReadFromJsonAsync<ListResponseModel<BulkDeleteResponseModel>>();
Assert.NotNull(results);
var index = 0;

View File

@ -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.Helpers;
using Bit.Api.Models.Response;
using Bit.Api.SecretManagerFeatures.Models.Request;
using Bit.Api.SecretManagerFeatures.Models.Response;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Test.Common.Helpers;
using Xunit;
@ -13,49 +15,43 @@ namespace Bit.Api.IntegrationTest.Controllers;
public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private readonly string _mockEncryptedString =
private const string _mockEncryptedString =
"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=";
private const string _mockNewName =
"2.3AZ+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=";
private readonly IAccessPolicyRepository _accessPolicyRepository;
private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory;
private readonly IServiceAccountRepository _serviceAccountRepository;
private Organization _organization = null!;
public ServiceAccountsControllerTest(ApiApplicationFactory factory)
{
_factory = factory;
_client = _factory.CreateClient();
_serviceAccountRepository = _factory.GetService<IServiceAccountRepository>();
_accessPolicyRepository = _factory.GetService<IAccessPolicyRepository>();
}
public async Task InitializeAsync()
{
var ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
var tokens = await _factory.LoginWithNewAccount(ownerEmail);
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, ownerEmail: ownerEmail, billingEmail: ownerEmail);
await _factory.LoginWithNewAccount(ownerEmail);
(_organization, _) =
await OrganizationTestHelpers.SignUpAsync(_factory, ownerEmail: ownerEmail, billingEmail: ownerEmail);
var tokens = await _factory.LoginAsync(ownerEmail);
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
_organization = organization;
}
public Task DisposeAsync()
{
return Task.CompletedTask;
}
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task GetServiceAccountsByOrganization()
public async Task GetServiceAccountsByOrganization_Admin()
{
var serviceAccountsToCreate = 3;
var serviceAccountIds = new List<Guid>();
for (var i = 0; i < serviceAccountsToCreate; i++)
{
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = _organization.Id,
Name = _mockEncryptedString,
});
serviceAccountIds.Add(serviceAccount.Id);
}
var serviceAccountIds = await SetupGetServiceAccountsByOrganizationAsync();
var response = await _client.GetAsync($"/organizations/{_organization.Id}/service-accounts");
response.EnsureSuccessStatusCode();
@ -67,12 +63,54 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
}
[Fact]
public async Task CreateServiceAccount()
public async Task GetServiceAccountsByOrganization_User_Success()
{
var request = new ServiceAccountCreateRequestModel()
{
Name = _mockEncryptedString,
};
// Create a new account as a user
var user = await LoginAsNewOrgUserAsync();
var serviceAccountIds = await SetupGetServiceAccountsByOrganizationAsync();
var accessPolicies = serviceAccountIds.Select(
id => new UserServiceAccountAccessPolicy
{
OrganizationUserId = user.Id,
GrantedServiceAccountId = id,
Read = true,
Write = false,
}).Cast<BaseAccessPolicy>().ToList();
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
var response = await _client.GetAsync($"/organizations/{_organization.Id}/service-accounts");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ListResponseModel<ServiceAccountResponseModel>>();
Assert.NotNull(result);
Assert.NotEmpty(result!.Data);
Assert.Equal(serviceAccountIds.Count, result.Data.Count());
}
[Fact]
public async Task GetServiceAccountsByOrganization_User_NoPermission()
{
// Create a new account as a user
await LoginAsNewOrgUserAsync();
await SetupGetServiceAccountsByOrganizationAsync();
var response = await _client.GetAsync($"/organizations/{_organization.Id}/service-accounts");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<ListResponseModel<ServiceAccountResponseModel>>();
Assert.NotNull(result);
Assert.Empty(result!.Data);
}
[Fact]
public async Task CreateServiceAccount_Admin()
{
var request = new ServiceAccountCreateRequestModel { Name = _mockEncryptedString };
var response = await _client.PostAsJsonAsync($"/organizations/{_organization.Id}/service-accounts", request);
response.EnsureSuccessStatusCode();
@ -91,7 +129,19 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
}
[Fact]
public async Task UpdateServiceAccount()
public async Task CreateServiceAccount_User_NoPermissions()
{
// Create a new account as a user
await LoginAsNewOrgUserAsync();
var request = new ServiceAccountCreateRequestModel { Name = _mockEncryptedString };
var response = await _client.PostAsJsonAsync($"/organizations/{_organization.Id}/service-accounts", request);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task UpdateServiceAccount_Admin()
{
var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
@ -99,10 +149,7 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
Name = _mockEncryptedString,
});
var request = new ServiceAccountUpdateRequestModel()
{
Name = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=",
};
var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };
var response = await _client.PutAsJsonAsync($"/service-accounts/{initialServiceAccount.Id}", request);
response.EnsureSuccessStatusCode();
@ -123,7 +170,59 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
}
[Fact]
public async Task CreateServiceAccountAccessToken()
public async Task UpdateServiceAccount_User_WithPermission()
{
// Create a new account as a user
var user = await LoginAsNewOrgUserAsync();
var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = _organization.Id,
Name = _mockEncryptedString,
});
await CreateUserServiceAccountAccessPolicyAsync(user.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 UpdateServiceAccount_User_NoPermissions()
{
// Create a new account as a user
await LoginAsNewOrgUserAsync();
var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = _organization.Id,
Name = _mockEncryptedString,
});
var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };
var response = await _client.PutAsJsonAsync($"/service-accounts/{initialServiceAccount.Id}", request);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public async Task CreateServiceAccountAccessToken_Admin()
{
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
@ -132,12 +231,12 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
});
var mockExpiresAt = DateTime.UtcNow.AddDays(30);
var request = new AccessTokenCreateRequestModel()
var request = new AccessTokenCreateRequestModel
{
Name = _mockEncryptedString,
EncryptedPayload = _mockEncryptedString,
Key = _mockEncryptedString,
ExpireAt = mockExpiresAt
ExpireAt = mockExpiresAt,
};
var response = await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-tokens", request);
@ -153,7 +252,67 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
}
[Fact]
public async Task CreateServiceAccountAccessTokenExpireAtNullAsync()
public async Task CreateServiceAccountAccessToken_User_WithPermission()
{
// Create a new account as a user
var user = await LoginAsNewOrgUserAsync();
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = _organization.Id,
Name = _mockEncryptedString,
});
await CreateUserServiceAccountAccessPolicyAsync(user.Id, serviceAccount.Id, true, true);
var mockExpiresAt = DateTime.UtcNow.AddDays(30);
var request = new AccessTokenCreateRequestModel
{
Name = _mockEncryptedString,
EncryptedPayload = _mockEncryptedString,
Key = _mockEncryptedString,
ExpireAt = mockExpiresAt,
};
var response = await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-tokens", request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<AccessTokenCreationResponseModel>();
Assert.NotNull(result);
Assert.Equal(request.Name, result!.Name);
Assert.NotNull(result.ClientSecret);
Assert.Equal(mockExpiresAt, result.ExpireAt);
AssertHelper.AssertRecent(result.RevisionDate);
AssertHelper.AssertRecent(result.CreationDate);
}
[Fact]
public async Task CreateServiceAccountAccessToken_User_NoPermission()
{
// Create a new account as a user
await LoginAsNewOrgUserAsync();
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = _organization.Id,
Name = _mockEncryptedString,
});
var mockExpiresAt = DateTime.UtcNow.AddDays(30);
var request = new AccessTokenCreateRequestModel
{
Name = _mockEncryptedString,
EncryptedPayload = _mockEncryptedString,
Key = _mockEncryptedString,
ExpireAt = mockExpiresAt,
};
var response = await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-tokens", request);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public async Task CreateServiceAccountAccessTokenExpireAtNullAsync_Admin()
{
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
@ -161,12 +320,12 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
Name = _mockEncryptedString,
});
var request = new AccessTokenCreateRequestModel()
var request = new AccessTokenCreateRequestModel
{
Name = _mockEncryptedString,
EncryptedPayload = _mockEncryptedString,
Key = _mockEncryptedString,
ExpireAt = null
ExpireAt = null,
};
var response = await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-tokens", request);
@ -180,4 +339,105 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
AssertHelper.AssertRecent(result.RevisionDate);
AssertHelper.AssertRecent(result.CreationDate);
}
[Fact]
public async Task CreateServiceAccountAccessTokenExpireAtNullAsync_User_WithPermission()
{
// Create a new account as a user
var user = await LoginAsNewOrgUserAsync();
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = _organization.Id,
Name = _mockEncryptedString,
});
await CreateUserServiceAccountAccessPolicyAsync(user.Id, serviceAccount.Id, true, true);
var request = new AccessTokenCreateRequestModel
{
Name = _mockEncryptedString,
EncryptedPayload = _mockEncryptedString,
Key = _mockEncryptedString,
ExpireAt = null,
};
var response = await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-tokens", request);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<AccessTokenCreationResponseModel>();
Assert.NotNull(result);
Assert.Equal(request.Name, result!.Name);
Assert.NotNull(result.ClientSecret);
Assert.Null(result.ExpireAt);
AssertHelper.AssertRecent(result.RevisionDate);
AssertHelper.AssertRecent(result.CreationDate);
}
[Fact]
public async Task CreateServiceAccountAccessTokenExpireAtNullAsync_User_NoPermission()
{
// Create a new account as a user
await LoginAsNewOrgUserAsync();
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = _organization.Id,
Name = _mockEncryptedString,
});
var request = new AccessTokenCreateRequestModel
{
Name = _mockEncryptedString,
EncryptedPayload = _mockEncryptedString,
Key = _mockEncryptedString,
ExpireAt = null,
};
var response = await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-tokens", request);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
private async Task<List<Guid>> SetupGetServiceAccountsByOrganizationAsync()
{
const int serviceAccountsToCreate = 3;
var serviceAccountIds = new List<Guid>();
for (var i = 0; i < serviceAccountsToCreate; i++)
{
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{
OrganizationId = _organization.Id,
Name = _mockEncryptedString,
});
serviceAccountIds.Add(serviceAccount.Id);
}
return serviceAccountIds;
}
private async Task CreateUserServiceAccountAccessPolicyAsync(Guid userId, Guid serviceAccountId, bool read,
bool write)
{
var accessPolicies = new List<BaseAccessPolicy>
{
new UserServiceAccountAccessPolicy
{
OrganizationUserId = userId,
GrantedServiceAccountId = serviceAccountId,
Read = read,
Write = write,
},
};
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
}
private async Task<OrganizationUser> LoginAsNewOrgUserAsync(OrganizationUserType type = OrganizationUserType.User)
{
var email = $"integration-test{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(email);
var orgUser = await OrganizationTestHelpers.CreateUserAsync(_factory, _organization.Id, email, type);
var tokens = await _factory.LoginAsync(email);
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
return orgUser;
}
}

View File

@ -1,13 +1,18 @@
using Bit.Api.Controllers;
using Bit.Api.SecretManagerFeatures.Models.Request;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.SecretManagerFeatures.AccessTokens.Interfaces;
using Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces;
using Bit.Core.Services;
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.Api.Test.Controllers;
@ -21,10 +26,11 @@ public class ServiceAccountsControllerTests
[BitAutoData]
public async void GetServiceAccountsByOrganization_ReturnsEmptyList(SutProvider<ServiceAccountsController> sutProvider, Guid id)
{
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
var result = await sutProvider.Sut.GetServiceAccountsByOrganizationAsync(id);
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)));
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), Arg.Any<Guid>(), Arg.Any<AccessClientType>());
Assert.Empty(result.Data);
}
@ -33,12 +39,15 @@ public class ServiceAccountsControllerTests
[BitAutoData]
public async void GetServiceAccountsByOrganization_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccount resultServiceAccount)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetManyByOrganizationIdAsync(default).ReturnsForAnyArgs(new List<ServiceAccount>() { resultServiceAccount });
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
sutProvider.GetDependency<IServiceAccountRepository>().GetManyByOrganizationIdAsync(default, default, default).ReturnsForAnyArgs(new List<ServiceAccount>() { resultServiceAccount });
var result = await sutProvider.Sut.GetServiceAccountsByOrganizationAsync(resultServiceAccount.OrganizationId);
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultServiceAccount.OrganizationId)));
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultServiceAccount.OrganizationId)), Arg.Any<Guid>(), Arg.Any<AccessClientType>());
Assert.NotEmpty(result.Data);
Assert.Single(result.Data);
}
@ -46,8 +55,8 @@ public class ServiceAccountsControllerTests
[BitAutoData]
public async void CreateServiceAccount_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountCreateRequestModel data, Guid organizationId)
{
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(true);
var resultServiceAccount = data.ToServiceAccount(organizationId);
sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default).ReturnsForAnyArgs(resultServiceAccount);
var result = await sutProvider.Sut.CreateServiceAccountAsync(organizationId, data);
@ -55,28 +64,113 @@ public class ServiceAccountsControllerTests
.CreateAsync(Arg.Any<ServiceAccount>());
}
[Theory]
[BitAutoData]
public async void CreateServiceAccount_NotOrgUser_Throws(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountCreateRequestModel data, Guid organizationId)
{
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(false);
var resultServiceAccount = data.ToServiceAccount(organizationId);
sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default).ReturnsForAnyArgs(resultServiceAccount);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.CreateServiceAccountAsync(organizationId, data));
await sutProvider.GetDependency<ICreateServiceAccountCommand>().DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any<ServiceAccount>());
}
[Theory]
[BitAutoData]
public async void UpdateServiceAccount_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountUpdateRequestModel data, Guid serviceAccountId)
{
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
var resultServiceAccount = data.ToServiceAccount(serviceAccountId);
sutProvider.GetDependency<IUpdateServiceAccountCommand>().UpdateAsync(default).ReturnsForAnyArgs(resultServiceAccount);
sutProvider.GetDependency<IUpdateServiceAccountCommand>().UpdateAsync(default, default).ReturnsForAnyArgs(resultServiceAccount);
var result = await sutProvider.Sut.UpdateServiceAccountAsync(serviceAccountId, data);
await sutProvider.GetDependency<IUpdateServiceAccountCommand>().Received(1)
.UpdateAsync(Arg.Any<ServiceAccount>());
.UpdateAsync(Arg.Any<ServiceAccount>(), Arg.Any<Guid>());
}
[Theory]
[BitAutoData]
public async void CreateAccessToken_Success(SutProvider<ServiceAccountsController> sutProvider, AccessTokenCreateRequestModel data, Guid serviceAccountId)
{
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
var resultAccessToken = data.ToApiKey(serviceAccountId);
sutProvider.GetDependency<ICreateAccessTokenCommand>().CreateAsync(default).ReturnsForAnyArgs(resultAccessToken);
sutProvider.GetDependency<ICreateAccessTokenCommand>().CreateAsync(default, default).ReturnsForAnyArgs(resultAccessToken);
var result = await sutProvider.Sut.CreateAccessTokenAsync(serviceAccountId, data);
await sutProvider.GetDependency<ICreateAccessTokenCommand>().Received(1)
.CreateAsync(Arg.Any<ApiKey>());
.CreateAsync(Arg.Any<ApiKey>(), Arg.Any<Guid>());
}
[Theory]
[BitAutoData]
public async void GetAccessTokens_NoServiceAccount_ThrowsNotFound(SutProvider<ServiceAccountsController> sutProvider, Guid serviceAccountId)
{
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetAccessTokens(serviceAccountId));
await sutProvider.GetDependency<IApiKeyRepository>().DidNotReceiveWithAnyArgs().GetManyByServiceAccountIdAsync(Arg.Any<Guid>());
}
[Theory]
[BitAutoData]
public async void GetAccessTokens_Admin_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccount data, Guid userId, ICollection<ApiKey> resultApiKeys)
{
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data);
foreach (var apiKey in resultApiKeys)
{
apiKey.Scope = "[\"api.secrets\"]";
}
sutProvider.GetDependency<IApiKeyRepository>().GetManyByServiceAccountIdAsync(default).ReturnsForAnyArgs(resultApiKeys);
var result = await sutProvider.Sut.GetAccessTokens(data.Id);
await sutProvider.GetDependency<IApiKeyRepository>().Received(1).GetManyByServiceAccountIdAsync(Arg.Any<Guid>());
Assert.NotEmpty(result.Data);
Assert.Equal(resultApiKeys.Count, result.Data.Count());
}
[Theory]
[BitAutoData]
public async void GetAccessTokens_User_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccount data, Guid userId, ICollection<ApiKey> resultApiKeys)
{
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(false);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasReadAccessToServiceAccount(default, default).ReturnsForAnyArgs(true);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data);
foreach (var apiKey in resultApiKeys)
{
apiKey.Scope = "[\"api.secrets\"]";
}
sutProvider.GetDependency<IApiKeyRepository>().GetManyByServiceAccountIdAsync(default).ReturnsForAnyArgs(resultApiKeys);
var result = await sutProvider.Sut.GetAccessTokens(data.Id);
await sutProvider.GetDependency<IApiKeyRepository>().Received(1).GetManyByServiceAccountIdAsync(Arg.Any<Guid>());
Assert.NotEmpty(result.Data);
Assert.Equal(resultApiKeys.Count, result.Data.Count());
}
[Theory]
[BitAutoData]
public async void GetAccessTokens_UserNoAccess_ThrowsNotFound(SutProvider<ServiceAccountsController> sutProvider, ServiceAccount data, Guid userId, ICollection<ApiKey> resultApiKeys)
{
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<ICurrentContext>().OrganizationAdmin(data.OrganizationId).Returns(false);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasReadAccessToServiceAccount(default, default).ReturnsForAnyArgs(false);
sutProvider.GetDependency<IServiceAccountRepository>().GetByIdAsync(default).ReturnsForAnyArgs(data);
foreach (var apiKey in resultApiKeys)
{
apiKey.Scope = "[\"api.secrets\"]";
}
sutProvider.GetDependency<IApiKeyRepository>().GetManyByServiceAccountIdAsync(default).ReturnsForAnyArgs(resultApiKeys);
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetAccessTokens(data.Id));
await sutProvider.GetDependency<IApiKeyRepository>().DidNotReceiveWithAnyArgs().GetManyByServiceAccountIdAsync(Arg.Any<Guid>());
}
}