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:
parent
bdea036c1f
commit
aa9f859306
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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)));
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -35,4 +35,3 @@ public class ServiceAccountResponseModel : ResponseModel
|
||||
|
||||
public DateTime RevisionDate { get; set; }
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>());
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user