1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-25 12:45:18 +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.Repositories;
using Bit.Core.SecretManagerFeatures.AccessTokens.Interfaces; using Bit.Core.SecretManagerFeatures.AccessTokens.Interfaces;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -7,16 +10,45 @@ namespace Bit.Commercial.Core.SecretManagerFeatures.AccessTokens;
public class CreateAccessTokenCommand : ICreateAccessTokenCommand public class CreateAccessTokenCommand : ICreateAccessTokenCommand
{ {
private readonly int _clientSecretMaxLength = 30;
private readonly IApiKeyRepository _apiKeyRepository; 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; _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); apiKey.ClientSecret = CoreHelpers.SecureRandomString(_clientSecretMaxLength);
return await _apiKeyRepository.CreateAsync(apiKey); 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.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces; using Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces;
@ -8,22 +10,38 @@ namespace Bit.Commercial.Core.SecretManagerFeatures.ServiceAccounts;
public class UpdateServiceAccountCommand : IUpdateServiceAccountCommand public class UpdateServiceAccountCommand : IUpdateServiceAccountCommand
{ {
private readonly IServiceAccountRepository _serviceAccountRepository; private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly ICurrentContext _currentContext;
public UpdateServiceAccountCommand(IServiceAccountRepository serviceAccountRepository) public UpdateServiceAccountCommand(IServiceAccountRepository serviceAccountRepository, ICurrentContext currentContext)
{ {
_serviceAccountRepository = serviceAccountRepository; _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); var serviceAccount = await _serviceAccountRepository.GetByIdAsync(updatedServiceAccount.Id);
if (existingServiceAccount == null) if (serviceAccount == null)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
serviceAccount.OrganizationId = existingServiceAccount.OrganizationId; var orgAdmin = await _currentContext.OrganizationAdmin(serviceAccount.OrganizationId);
serviceAccount.CreationDate = existingServiceAccount.CreationDate; 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; serviceAccount.RevisionDate = DateTime.UtcNow;
await _serviceAccountRepository.ReplaceAsync(serviceAccount); await _serviceAccountRepository.ReplaceAsync(serviceAccount);

View File

@ -31,12 +31,25 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli
await dbContext.AddAsync(entity); await dbContext.AddAsync(entity);
break; break;
} }
case Core.Entities.UserServiceAccountAccessPolicy accessPolicy:
{
var entity =
Mapper.Map<UserServiceAccountAccessPolicy>(accessPolicy);
await dbContext.AddAsync(entity);
break;
}
case Core.Entities.GroupProjectAccessPolicy accessPolicy: case Core.Entities.GroupProjectAccessPolicy accessPolicy:
{ {
var entity = Mapper.Map<GroupProjectAccessPolicy>(accessPolicy); var entity = Mapper.Map<GroupProjectAccessPolicy>(accessPolicy);
await dbContext.AddAsync(entity); await dbContext.AddAsync(entity);
break; break;
} }
case Core.Entities.GroupServiceAccountAccessPolicy accessPolicy:
{
var entity = Mapper.Map<GroupServiceAccountAccessPolicy>(accessPolicy);
await dbContext.AddAsync(entity);
break;
}
case Core.Entities.ServiceAccountProjectAccessPolicy accessPolicy: case Core.Entities.ServiceAccountProjectAccessPolicy accessPolicy:
{ {
var entity = Mapper.Map<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.Core.Repositories;
using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Models;
using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories;
@ -13,16 +15,50 @@ public class ServiceAccountRepository : Repository<Core.Entities.ServiceAccount,
: base(serviceScopeFactory, mapper, db => db.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 dbContext = GetDatabaseContext(scope);
var serviceAccounts = await dbContext.ServiceAccount var query = dbContext.ServiceAccount.Where(c => c.OrganizationId == organizationId);
.Where(c => c.OrganizationId == organizationId)
.OrderBy(c => c.RevisionDate) query = accessType switch
.ToListAsync(); {
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); 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.Commercial.Core.SecretManagerFeatures.AccessTokens;
using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
@ -14,10 +16,57 @@ public class CreateServiceAccountCommandTests
{ {
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task CreateAsync_CallsCreate(ApiKey data, public async Task CreateAsync_NoServiceAccountId_ThrowsBadRequestException(ApiKey data, Guid userId,
SutProvider<CreateAccessTokenCommand> sutProvider) 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) await sutProvider.GetDependency<IApiKeyRepository>().Received(1)
.CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data))); .CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));

View File

@ -1,4 +1,5 @@
using Bit.Commercial.Core.SecretManagerFeatures.ServiceAccounts; using Bit.Commercial.Core.SecretManagerFeatures.ServiceAccounts;
using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -15,19 +16,47 @@ public class UpdateServiceAccountCommandTests
{ {
[Theory] [Theory]
[BitAutoData] [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); await sutProvider.GetDependency<IServiceAccountRepository>().DidNotReceiveWithAnyArgs().ReplaceAsync(default);
} }
[Theory] [Theory]
[BitAutoData] [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); 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) await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)
.ReplaceAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data))); .ReplaceAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)));
@ -35,9 +64,10 @@ public class UpdateServiceAccountCommandTests
[Theory] [Theory]
[BitAutoData] [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>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);
var updatedOrgId = Guid.NewGuid(); var updatedOrgId = Guid.NewGuid();
var serviceAccountUpdate = new ServiceAccount() var serviceAccountUpdate = new ServiceAccount()
@ -47,7 +77,7 @@ public class UpdateServiceAccountCommandTests
Name = existingServiceAccount.Name, 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.Equal(existingServiceAccount.OrganizationId, result.OrganizationId);
Assert.NotEqual(existingServiceAccount.OrganizationId, updatedOrgId); Assert.NotEqual(existingServiceAccount.OrganizationId, updatedOrgId);
@ -55,9 +85,10 @@ public class UpdateServiceAccountCommandTests
[Theory] [Theory]
[BitAutoData] [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>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);
var updatedCreationDate = DateTime.UtcNow; var updatedCreationDate = DateTime.UtcNow;
var serviceAccountUpdate = new ServiceAccount() var serviceAccountUpdate = new ServiceAccount()
@ -67,7 +98,7 @@ public class UpdateServiceAccountCommandTests
Name = existingServiceAccount.Name, 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.Equal(existingServiceAccount.CreationDate, result.CreationDate);
Assert.NotEqual(existingServiceAccount.CreationDate, updatedCreationDate); Assert.NotEqual(existingServiceAccount.CreationDate, updatedCreationDate);
@ -75,9 +106,10 @@ public class UpdateServiceAccountCommandTests
[Theory] [Theory]
[BitAutoData] [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>().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount);
sutProvider.GetDependency<IServiceAccountRepository>().UserHasWriteAccessToServiceAccount(existingServiceAccount.Id, userId).Returns(true);
var updatedRevisionDate = DateTime.UtcNow.AddDays(10); var updatedRevisionDate = DateTime.UtcNow.AddDays(10);
var serviceAccountUpdate = new ServiceAccount() var serviceAccountUpdate = new ServiceAccount()
@ -87,9 +119,9 @@ public class UpdateServiceAccountCommandTests
Name = existingServiceAccount.Name, 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); 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.Request;
using Bit.Api.SecretManagerFeatures.Models.Response; using Bit.Api.SecretManagerFeatures.Models.Response;
using Bit.Api.Utilities; using Bit.Api.Utilities;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.SecretManagerFeatures.AccessTokens.Interfaces; using Bit.Core.SecretManagerFeatures.AccessTokens.Interfaces;
using Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces; using Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces;
using Bit.Core.Services;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Controllers; namespace Bit.Api.Controllers;
@ -14,59 +18,105 @@ namespace Bit.Api.Controllers;
[Route("service-accounts")] [Route("service-accounts")]
public class ServiceAccountsController : Controller public class ServiceAccountsController : Controller
{ {
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IApiKeyRepository _apiKeyRepository; private readonly IApiKeyRepository _apiKeyRepository;
private readonly ICreateServiceAccountCommand _createServiceAccountCommand;
private readonly ICreateAccessTokenCommand _createAccessTokenCommand; private readonly ICreateAccessTokenCommand _createAccessTokenCommand;
private readonly ICreateServiceAccountCommand _createServiceAccountCommand;
private readonly ICurrentContext _currentContext;
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand; private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand;
private readonly IUserService _userService;
public ServiceAccountsController( public ServiceAccountsController(
IUserService userService,
IServiceAccountRepository serviceAccountRepository, IServiceAccountRepository serviceAccountRepository,
ICreateAccessTokenCommand createAccessTokenCommand, ICreateAccessTokenCommand createAccessTokenCommand,
IApiKeyRepository apiKeyRepository, ICreateServiceAccountCommand createServiceAccountCommand, IApiKeyRepository apiKeyRepository, ICreateServiceAccountCommand createServiceAccountCommand,
IUpdateServiceAccountCommand updateServiceAccountCommand) IUpdateServiceAccountCommand updateServiceAccountCommand,
ICurrentContext currentContext)
{ {
_userService = userService;
_serviceAccountRepository = serviceAccountRepository; _serviceAccountRepository = serviceAccountRepository;
_apiKeyRepository = apiKeyRepository; _apiKeyRepository = apiKeyRepository;
_createServiceAccountCommand = createServiceAccountCommand; _createServiceAccountCommand = createServiceAccountCommand;
_updateServiceAccountCommand = updateServiceAccountCommand; _updateServiceAccountCommand = updateServiceAccountCommand;
_createAccessTokenCommand = createAccessTokenCommand; _createAccessTokenCommand = createAccessTokenCommand;
_currentContext = currentContext;
} }
[HttpGet("/organizations/{organizationId}/service-accounts")] [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)); var responses = serviceAccounts.Select(serviceAccount => new ServiceAccountResponseModel(serviceAccount));
return new ListResponseModel<ServiceAccountResponseModel>(responses); return new ListResponseModel<ServiceAccountResponseModel>(responses);
} }
[HttpPost("/organizations/{organizationId}/service-accounts")] [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)); var result = await _createServiceAccountCommand.CreateAsync(createRequest.ToServiceAccount(organizationId));
return new ServiceAccountResponseModel(result); return new ServiceAccountResponseModel(result);
} }
[HttpPut("{id}")] [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); return new ServiceAccountResponseModel(result);
} }
[HttpGet("{id}/access-tokens")] [HttpGet("{id}/access-tokens")]
public async Task<ListResponseModel<AccessTokenResponseModel>> GetAccessTokens([FromRoute] Guid id) 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 accessTokens = await _apiKeyRepository.GetManyByServiceAccountIdAsync(id);
var responses = accessTokens.Select(token => new AccessTokenResponseModel(token)); var responses = accessTokens.Select(token => new AccessTokenResponseModel(token));
return new ListResponseModel<AccessTokenResponseModel>(responses); return new ListResponseModel<AccessTokenResponseModel>(responses);
} }
[HttpPost("{id}/access-tokens")] [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); return new AccessTokenCreationResponseModel(result);
} }
} }

View File

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

View File

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

View File

@ -1,11 +1,14 @@
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums;
namespace Bit.Core.Repositories; namespace Bit.Core.Repositories;
public interface IServiceAccountRepository 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> GetByIdAsync(Guid id);
Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount); Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount);
Task ReplaceAsync(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 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 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() CreateMap<Core.Entities.UserProjectAccessPolicy, UserProjectAccessPolicy>().ReverseMap()
.ForMember(dst => dst.User, opt => opt.MapFrom(src => src.OrganizationUser.User)); .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.GroupProjectAccessPolicy, GroupProjectAccessPolicy>().ReverseMap();
CreateMap<Core.Entities.GroupServiceAccountAccessPolicy, GroupServiceAccountAccessPolicy>().ReverseMap();
CreateMap<Core.Entities.ServiceAccountProjectAccessPolicy, ServiceAccountProjectAccessPolicy>().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 class ServiceAccount : Core.Entities.ServiceAccount
{ {
public virtual Organization Organization { get; set; } public virtual Organization Organization { get; set; }
public virtual ICollection<GroupServiceAccountAccessPolicy> GroupAccessPolicies { get; set; }
public virtual ICollection<UserServiceAccountAccessPolicy> UserAccessPolicies { get; set; }
} }
public class ServiceAccountMapperProfile : Profile public class ServiceAccountMapperProfile : Profile

View File

@ -215,7 +215,6 @@ public class ProjectsControllerTest : IClassFixture<ApiApplicationFactory>, IAsy
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var results = await response.Content.ReadFromJsonAsync<ListResponseModel<BulkDeleteResponseModel>>(); var results = await response.Content.ReadFromJsonAsync<ListResponseModel<BulkDeleteResponseModel>>();
Assert.NotNull(results); Assert.NotNull(results);
var index = 0; 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.Factories;
using Bit.Api.IntegrationTest.Helpers; using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Api.SecretManagerFeatures.Models.Request; using Bit.Api.SecretManagerFeatures.Models.Request;
using Bit.Api.SecretManagerFeatures.Models.Response; using Bit.Api.SecretManagerFeatures.Models.Response;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Test.Common.Helpers; using Bit.Test.Common.Helpers;
using Xunit; using Xunit;
@ -13,49 +15,43 @@ namespace Bit.Api.IntegrationTest.Controllers;
public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyncLifetime public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{ {
private readonly string _mockEncryptedString = private const string _mockEncryptedString =
"2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; "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 HttpClient _client;
private readonly ApiApplicationFactory _factory; private readonly ApiApplicationFactory _factory;
private readonly IServiceAccountRepository _serviceAccountRepository; private readonly IServiceAccountRepository _serviceAccountRepository;
private Organization _organization = null!; private Organization _organization = null!;
public ServiceAccountsControllerTest(ApiApplicationFactory factory) public ServiceAccountsControllerTest(ApiApplicationFactory factory)
{ {
_factory = factory; _factory = factory;
_client = _factory.CreateClient(); _client = _factory.CreateClient();
_serviceAccountRepository = _factory.GetService<IServiceAccountRepository>(); _serviceAccountRepository = _factory.GetService<IServiceAccountRepository>();
_accessPolicyRepository = _factory.GetService<IAccessPolicyRepository>();
} }
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
var ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; var ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
var tokens = await _factory.LoginWithNewAccount(ownerEmail); await _factory.LoginWithNewAccount(ownerEmail);
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, ownerEmail: ownerEmail, billingEmail: ownerEmail); (_organization, _) =
await OrganizationTestHelpers.SignUpAsync(_factory, ownerEmail: ownerEmail, billingEmail: ownerEmail);
var tokens = await _factory.LoginAsync(ownerEmail);
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
_organization = organization;
} }
public Task DisposeAsync() public Task DisposeAsync() => Task.CompletedTask;
{
return Task.CompletedTask;
}
[Fact] [Fact]
public async Task GetServiceAccountsByOrganization() public async Task GetServiceAccountsByOrganization_Admin()
{ {
var serviceAccountsToCreate = 3; var serviceAccountIds = await SetupGetServiceAccountsByOrganizationAsync();
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 response = await _client.GetAsync($"/organizations/{_organization.Id}/service-accounts"); var response = await _client.GetAsync($"/organizations/{_organization.Id}/service-accounts");
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
@ -67,12 +63,54 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
} }
[Fact] [Fact]
public async Task CreateServiceAccount() public async Task GetServiceAccountsByOrganization_User_Success()
{ {
var request = new ServiceAccountCreateRequestModel() // Create a new account as a user
var user = await LoginAsNewOrgUserAsync();
var serviceAccountIds = await SetupGetServiceAccountsByOrganizationAsync();
var accessPolicies = serviceAccountIds.Select(
id => new UserServiceAccountAccessPolicy
{ {
Name = _mockEncryptedString, 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); var response = await _client.PostAsJsonAsync($"/organizations/{_organization.Id}/service-accounts", request);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
@ -91,7 +129,19 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
} }
[Fact] [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 var initialServiceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{ {
@ -99,10 +149,7 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
Name = _mockEncryptedString, Name = _mockEncryptedString,
}); });
var request = new ServiceAccountUpdateRequestModel() var request = new ServiceAccountUpdateRequestModel { Name = _mockNewName };
{
Name = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=",
};
var response = await _client.PutAsJsonAsync($"/service-accounts/{initialServiceAccount.Id}", request); var response = await _client.PutAsJsonAsync($"/service-accounts/{initialServiceAccount.Id}", request);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
@ -123,7 +170,59 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
} }
[Fact] [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 var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{ {
@ -132,12 +231,12 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
}); });
var mockExpiresAt = DateTime.UtcNow.AddDays(30); var mockExpiresAt = DateTime.UtcNow.AddDays(30);
var request = new AccessTokenCreateRequestModel() var request = new AccessTokenCreateRequestModel
{ {
Name = _mockEncryptedString, Name = _mockEncryptedString,
EncryptedPayload = _mockEncryptedString, EncryptedPayload = _mockEncryptedString,
Key = _mockEncryptedString, Key = _mockEncryptedString,
ExpireAt = mockExpiresAt ExpireAt = mockExpiresAt,
}; };
var response = await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-tokens", request); var response = await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-tokens", request);
@ -153,7 +252,67 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
} }
[Fact] [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 var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
{ {
@ -161,12 +320,12 @@ public class ServiceAccountsControllerTest : IClassFixture<ApiApplicationFactory
Name = _mockEncryptedString, Name = _mockEncryptedString,
}); });
var request = new AccessTokenCreateRequestModel() var request = new AccessTokenCreateRequestModel
{ {
Name = _mockEncryptedString, Name = _mockEncryptedString,
EncryptedPayload = _mockEncryptedString, EncryptedPayload = _mockEncryptedString,
Key = _mockEncryptedString, Key = _mockEncryptedString,
ExpireAt = null ExpireAt = null,
}; };
var response = await _client.PostAsJsonAsync($"/service-accounts/{serviceAccount.Id}/access-tokens", request); 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.RevisionDate);
AssertHelper.AssertRecent(result.CreationDate); 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.Controllers;
using Bit.Api.SecretManagerFeatures.Models.Request; using Bit.Api.SecretManagerFeatures.Models.Request;
using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.SecretManagerFeatures.AccessTokens.Interfaces; using Bit.Core.SecretManagerFeatures.AccessTokens.Interfaces;
using Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces; using Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers; using Bit.Test.Common.Helpers;
using NSubstitute; using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit; using Xunit;
namespace Bit.Api.Test.Controllers; namespace Bit.Api.Test.Controllers;
@ -21,10 +26,11 @@ public class ServiceAccountsControllerTests
[BitAutoData] [BitAutoData]
public async void GetServiceAccountsByOrganization_ReturnsEmptyList(SutProvider<ServiceAccountsController> sutProvider, Guid id) 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); var result = await sutProvider.Sut.GetServiceAccountsByOrganizationAsync(id);
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1) 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); Assert.Empty(result.Data);
} }
@ -33,12 +39,15 @@ public class ServiceAccountsControllerTests
[BitAutoData] [BitAutoData]
public async void GetServiceAccountsByOrganization_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccount resultServiceAccount) 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); var result = await sutProvider.Sut.GetServiceAccountsByOrganizationAsync(resultServiceAccount.OrganizationId);
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1) 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] [BitAutoData]
public async void CreateServiceAccount_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountCreateRequestModel data, Guid organizationId) public async void CreateServiceAccount_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountCreateRequestModel data, Guid organizationId)
{ {
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(true);
var resultServiceAccount = data.ToServiceAccount(organizationId); var resultServiceAccount = data.ToServiceAccount(organizationId);
sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default).ReturnsForAnyArgs(resultServiceAccount); sutProvider.GetDependency<ICreateServiceAccountCommand>().CreateAsync(default).ReturnsForAnyArgs(resultServiceAccount);
var result = await sutProvider.Sut.CreateServiceAccountAsync(organizationId, data); var result = await sutProvider.Sut.CreateServiceAccountAsync(organizationId, data);
@ -55,28 +64,113 @@ public class ServiceAccountsControllerTests
.CreateAsync(Arg.Any<ServiceAccount>()); .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] [Theory]
[BitAutoData] [BitAutoData]
public async void UpdateServiceAccount_Success(SutProvider<ServiceAccountsController> sutProvider, ServiceAccountUpdateRequestModel data, Guid serviceAccountId) 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); 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); var result = await sutProvider.Sut.UpdateServiceAccountAsync(serviceAccountId, data);
await sutProvider.GetDependency<IUpdateServiceAccountCommand>().Received(1) await sutProvider.GetDependency<IUpdateServiceAccountCommand>().Received(1)
.UpdateAsync(Arg.Any<ServiceAccount>()); .UpdateAsync(Arg.Any<ServiceAccount>(), Arg.Any<Guid>());
} }
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async void CreateAccessToken_Success(SutProvider<ServiceAccountsController> sutProvider, AccessTokenCreateRequestModel data, Guid serviceAccountId) 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); 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); var result = await sutProvider.Sut.CreateAccessTokenAsync(serviceAccountId, data);
await sutProvider.GetDependency<ICreateAccessTokenCommand>().Received(1) 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>());
} }
} }