diff --git a/bitwarden_license/src/Commercial.Core/SecretManagerFeatures/AccessTokens/CreateAccessTokenCommand.cs b/bitwarden_license/src/Commercial.Core/SecretManagerFeatures/AccessTokens/CreateAccessTokenCommand.cs index 3d1cf574b..0208fca71 100644 --- a/bitwarden_license/src/Commercial.Core/SecretManagerFeatures/AccessTokens/CreateAccessTokenCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretManagerFeatures/AccessTokens/CreateAccessTokenCommand.cs @@ -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 CreateAsync(ApiKey apiKey) + public async Task 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); } diff --git a/bitwarden_license/src/Commercial.Core/SecretManagerFeatures/ServiceAccounts/UpdateServiceAccountCommand.cs b/bitwarden_license/src/Commercial.Core/SecretManagerFeatures/ServiceAccounts/UpdateServiceAccountCommand.cs index 2fad0567a..895f0f500 100644 --- a/bitwarden_license/src/Commercial.Core/SecretManagerFeatures/ServiceAccounts/UpdateServiceAccountCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretManagerFeatures/ServiceAccounts/UpdateServiceAccountCommand.cs @@ -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 UpdateAsync(ServiceAccount serviceAccount) + public async Task 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); diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/Repositories/AccessPolicyRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/Repositories/AccessPolicyRepository.cs index 9d1d135f4..db6c008c7 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/Repositories/AccessPolicyRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/Repositories/AccessPolicyRepository.cs @@ -31,12 +31,25 @@ public class AccessPolicyRepository : BaseEntityFrameworkRepository, IAccessPoli await dbContext.AddAsync(entity); break; } + case Core.Entities.UserServiceAccountAccessPolicy accessPolicy: + { + var entity = + Mapper.Map(accessPolicy); + await dbContext.AddAsync(entity); + break; + } case Core.Entities.GroupProjectAccessPolicy accessPolicy: { var entity = Mapper.Map(accessPolicy); await dbContext.AddAsync(entity); break; } + case Core.Entities.GroupServiceAccountAccessPolicy accessPolicy: + { + var entity = Mapper.Map(accessPolicy); + await dbContext.AddAsync(entity); + break; + } case Core.Entities.ServiceAccountProjectAccessPolicy accessPolicy: { var entity = Mapper.Map(accessPolicy); diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/Repositories/ServiceAccountRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/Repositories/ServiceAccountRepository.cs index e04d65ce8..54640d65f 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/Repositories/ServiceAccountRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/Repositories/ServiceAccountRepository.cs @@ -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 db.ServiceAccount) { } - public async Task> GetManyByOrganizationIdAsync(Guid organizationId) + public async Task> 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>(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>(serviceAccounts); } + + public async Task 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 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> 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> 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)); } diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretManagerFeatures/AccessTokens/CreateAccessTokenCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretManagerFeatures/AccessTokens/CreateAccessTokenCommandTests.cs index e9a9c7ba8..804b343fe 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretManagerFeatures/AccessTokens/CreateAccessTokenCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretManagerFeatures/AccessTokens/CreateAccessTokenCommandTests.cs @@ -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 sutProvider) + public async Task CreateAsync_NoServiceAccountId_ThrowsBadRequestException(ApiKey data, Guid userId, + SutProvider sutProvider) { - await sutProvider.Sut.CreateAsync(data); + data.ServiceAccountId = null; + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(data, userId)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_User_NoAccess(ApiKey data, Guid userId, ServiceAccount saData, + SutProvider sutProvider) + { + data.ServiceAccountId = saData.Id; + + sutProvider.GetDependency().GetByIdAsync(saData.Id).Returns(saData); + sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(saData.Id, userId).Returns(false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(data, userId)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_User_Success(ApiKey data, Guid userId, ServiceAccount saData, + SutProvider sutProvider) + { + data.ServiceAccountId = saData.Id; + sutProvider.GetDependency().GetByIdAsync(saData.Id).Returns(saData); + sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(saData.Id, userId).Returns(true); + + await sutProvider.Sut.CreateAsync(data, userId); + + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data))); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_Admin_Succeeds(ApiKey data, Guid userId, ServiceAccount saData, + SutProvider sutProvider) + { + data.ServiceAccountId = saData.Id; + + sutProvider.GetDependency().GetByIdAsync(saData.Id).Returns(saData); + sutProvider.GetDependency().OrganizationAdmin(saData.OrganizationId).Returns(true); + + await sutProvider.Sut.CreateAsync(data, userId); await sutProvider.GetDependency().Received(1) .CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data))); diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretManagerFeatures/ServiceAccounts/UpdateServiceAccountCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretManagerFeatures/ServiceAccounts/UpdateServiceAccountCommandTests.cs index 3a3fd6c66..8014a682e 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretManagerFeatures/ServiceAccounts/UpdateServiceAccountCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretManagerFeatures/ServiceAccounts/UpdateServiceAccountCommandTests.cs @@ -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 sutProvider) + public async Task UpdateAsync_ServiceAccountDoesNotExist_ThrowsNotFound(ServiceAccount data, Guid userId, SutProvider sutProvider) { - var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data)); + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data, userId)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); } [Theory] [BitAutoData] - public async Task UpdateAsync_CallsReplaceAsync(ServiceAccount data, SutProvider sutProvider) + public async Task UpdateAsync_User_NoAccess(ServiceAccount data, Guid userId, SutProvider sutProvider) { sutProvider.GetDependency().GetByIdAsync(data.Id).Returns(data); - await sutProvider.Sut.UpdateAsync(data); + sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(data.Id, userId).Returns(false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data, userId)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + } + + [Theory] + [BitAutoData] + public async Task UpdateAsync_User_Success(ServiceAccount data, Guid userId, SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(data.Id).Returns(data); + sutProvider.GetDependency().UserHasWriteAccessToServiceAccount(data.Id, userId).Returns(true); + + await sutProvider.Sut.UpdateAsync(data, userId); + + await sutProvider.GetDependency().Received(1) + .ReplaceAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data))); + } + + + [Theory] + [BitAutoData] + public async Task UpdateAsync_Admin_Success(ServiceAccount data, Guid userId, SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(data.Id).Returns(data); + sutProvider.GetDependency().OrganizationAdmin(data.OrganizationId).Returns(true); + + await sutProvider.Sut.UpdateAsync(data, userId); await sutProvider.GetDependency().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 sutProvider) + public async Task UpdateAsync_DoesNotModifyOrganizationId(ServiceAccount existingServiceAccount, Guid userId, SutProvider sutProvider) { sutProvider.GetDependency().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount); + sutProvider.GetDependency().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 sutProvider) + public async Task UpdateAsync_DoesNotModifyCreationDate(ServiceAccount existingServiceAccount, Guid userId, SutProvider sutProvider) { sutProvider.GetDependency().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount); + sutProvider.GetDependency().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 sutProvider) + public async Task UpdateAsync_RevisionDateIsUpdatedToUtcNow(ServiceAccount existingServiceAccount, Guid userId, SutProvider sutProvider) { sutProvider.GetDependency().GetByIdAsync(existingServiceAccount.Id).Returns(existingServiceAccount); + sutProvider.GetDependency().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); } } diff --git a/src/Api/Controllers/ServiceAccountsController.cs b/src/Api/Controllers/ServiceAccountsController.cs index 353d9bba7..366802d23 100644 --- a/src/Api/Controllers/ServiceAccountsController.cs +++ b/src/Api/Controllers/ServiceAccountsController.cs @@ -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> GetServiceAccountsByOrganizationAsync([FromRoute] Guid organizationId) + public async Task> 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(responses); } [HttpPost("/organizations/{organizationId}/service-accounts")] - public async Task CreateServiceAccountAsync([FromRoute] Guid organizationId, [FromBody] ServiceAccountCreateRequestModel createRequest) + public async Task 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 UpdateServiceAccountAsync([FromRoute] Guid id, [FromBody] ServiceAccountUpdateRequestModel updateRequest) + public async Task 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> 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(responses); } [HttpPost("{id}/access-tokens")] - public async Task CreateAccessTokenAsync([FromRoute] Guid id, [FromBody] AccessTokenCreateRequestModel request) + public async Task 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); } } diff --git a/src/Api/Models/Response/SecretsManager/AccessTokenResponseModel.cs b/src/Api/Models/Response/SecretsManager/AccessTokenResponseModel.cs index 235678509..9c796f4d7 100644 --- a/src/Api/Models/Response/SecretsManager/AccessTokenResponseModel.cs +++ b/src/Api/Models/Response/SecretsManager/AccessTokenResponseModel.cs @@ -17,11 +17,11 @@ public class AccessTokenResponseModel : ResponseModel RevisionDate = apiKey.RevisionDate; } - public Guid Id { get; } - public string Name { get; } - public ICollection Scopes { get; } + public Guid Id { get; set; } + public string Name { get; set; } + public ICollection 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; } } diff --git a/src/Api/SecretManagerFeatures/Models/Response/ServiceAccountResponseModel.cs b/src/Api/SecretManagerFeatures/Models/Response/ServiceAccountResponseModel.cs index 258a06ed1..5c0c19b52 100644 --- a/src/Api/SecretManagerFeatures/Models/Response/ServiceAccountResponseModel.cs +++ b/src/Api/SecretManagerFeatures/Models/Response/ServiceAccountResponseModel.cs @@ -35,4 +35,3 @@ public class ServiceAccountResponseModel : ResponseModel public DateTime RevisionDate { get; set; } } - diff --git a/src/Core/Repositories/IServiceAccountRepository.cs b/src/Core/Repositories/IServiceAccountRepository.cs index 83da33a15..cd611936f 100644 --- a/src/Core/Repositories/IServiceAccountRepository.cs +++ b/src/Core/Repositories/IServiceAccountRepository.cs @@ -1,11 +1,14 @@ using Bit.Core.Entities; +using Bit.Core.Enums; namespace Bit.Core.Repositories; public interface IServiceAccountRepository { - Task> GetManyByOrganizationIdAsync(Guid organizationId); + Task> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType); Task GetByIdAsync(Guid id); Task CreateAsync(ServiceAccount serviceAccount); Task ReplaceAsync(ServiceAccount serviceAccount); + Task UserHasReadAccessToServiceAccount(Guid id, Guid userId); + Task UserHasWriteAccessToServiceAccount(Guid id, Guid userId); } diff --git a/src/Core/SecretManagerFeatures/AccessTokens/Interfaces/ICreateAccessTokenCommand.cs b/src/Core/SecretManagerFeatures/AccessTokens/Interfaces/ICreateAccessTokenCommand.cs index d4af3a3dc..b89fddc47 100644 --- a/src/Core/SecretManagerFeatures/AccessTokens/Interfaces/ICreateAccessTokenCommand.cs +++ b/src/Core/SecretManagerFeatures/AccessTokens/Interfaces/ICreateAccessTokenCommand.cs @@ -4,5 +4,5 @@ namespace Bit.Core.SecretManagerFeatures.AccessTokens.Interfaces; public interface ICreateAccessTokenCommand { - Task CreateAsync(ApiKey apiKey); + Task CreateAsync(ApiKey apiKey, Guid userId); } diff --git a/src/Core/SecretManagerFeatures/ServiceAccounts/Interfaces/IUpdateServiceAccountCommand.cs b/src/Core/SecretManagerFeatures/ServiceAccounts/Interfaces/IUpdateServiceAccountCommand.cs index 5d7f65261..629b259e2 100644 --- a/src/Core/SecretManagerFeatures/ServiceAccounts/Interfaces/IUpdateServiceAccountCommand.cs +++ b/src/Core/SecretManagerFeatures/ServiceAccounts/Interfaces/IUpdateServiceAccountCommand.cs @@ -4,5 +4,5 @@ namespace Bit.Core.SecretManagerFeatures.ServiceAccounts.Interfaces; public interface IUpdateServiceAccountCommand { - Task UpdateAsync(ServiceAccount serviceAccount); + Task UpdateAsync(ServiceAccount serviceAccount, Guid userId); } diff --git a/src/Infrastructure.EntityFramework/Models/AccessPolicy.cs b/src/Infrastructure.EntityFramework/Models/AccessPolicy.cs index 9be7edb19..843965eb6 100644 --- a/src/Infrastructure.EntityFramework/Models/AccessPolicy.cs +++ b/src/Infrastructure.EntityFramework/Models/AccessPolicy.cs @@ -13,7 +13,10 @@ public class AccessPolicyMapperProfile : Profile { CreateMap().ReverseMap() .ForMember(dst => dst.User, opt => opt.MapFrom(src => src.OrganizationUser.User)); + CreateMap().ReverseMap() + .ForMember(dst => dst.User, opt => opt.MapFrom(src => src.OrganizationUser.User)); CreateMap().ReverseMap(); + CreateMap().ReverseMap(); CreateMap().ReverseMap(); } } diff --git a/src/Infrastructure.EntityFramework/Models/ServiceAccount.cs b/src/Infrastructure.EntityFramework/Models/ServiceAccount.cs index 2b4992f52..5622cda8f 100644 --- a/src/Infrastructure.EntityFramework/Models/ServiceAccount.cs +++ b/src/Infrastructure.EntityFramework/Models/ServiceAccount.cs @@ -5,6 +5,8 @@ namespace Bit.Infrastructure.EntityFramework.Models; public class ServiceAccount : Core.Entities.ServiceAccount { public virtual Organization Organization { get; set; } + public virtual ICollection GroupAccessPolicies { get; set; } + public virtual ICollection UserAccessPolicies { get; set; } } public class ServiceAccountMapperProfile : Profile diff --git a/test/Api.IntegrationTest/Controllers/ProjectsControllerTest.cs b/test/Api.IntegrationTest/Controllers/ProjectsControllerTest.cs index 8c4c81e59..c1c4e048b 100644 --- a/test/Api.IntegrationTest/Controllers/ProjectsControllerTest.cs +++ b/test/Api.IntegrationTest/Controllers/ProjectsControllerTest.cs @@ -215,7 +215,6 @@ public class ProjectsControllerTest : IClassFixture, IAsy response.EnsureSuccessStatusCode(); var results = await response.Content.ReadFromJsonAsync>(); - Assert.NotNull(results); var index = 0; diff --git a/test/Api.IntegrationTest/Controllers/ServiceAccountsControllerTests.cs b/test/Api.IntegrationTest/Controllers/ServiceAccountsControllerTests.cs index 0fa48b321..5909cca81 100644 --- a/test/Api.IntegrationTest/Controllers/ServiceAccountsControllerTests.cs +++ b/test/Api.IntegrationTest/Controllers/ServiceAccountsControllerTests.cs @@ -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, 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(); + _accessPolicyRepository = _factory.GetService(); } 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(); - 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 new UserServiceAccountAccessPolicy + { + OrganizationUserId = user.Id, + GrantedServiceAccountId = id, + Read = true, + Write = false, + }).Cast().ToList(); + + + await _accessPolicyRepository.CreateManyAsync(accessPolicies); + + + var response = await _client.GetAsync($"/organizations/{_organization.Id}/service-accounts"); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadFromJsonAsync>(); + + 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>(); + + 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(); + 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(); + + 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(); + + 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> SetupGetServiceAccountsByOrganizationAsync() + { + const int serviceAccountsToCreate = 3; + var serviceAccountIds = new List(); + 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 + { + new UserServiceAccountAccessPolicy + { + OrganizationUserId = userId, + GrantedServiceAccountId = serviceAccountId, + Read = read, + Write = write, + }, + }; + await _accessPolicyRepository.CreateManyAsync(accessPolicies); + } + + private async Task 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; + } } diff --git a/test/Api.Test/Controllers/ServiceAccountsControllerTests.cs b/test/Api.Test/Controllers/ServiceAccountsControllerTests.cs index 720088e9a..3e0d8f851 100644 --- a/test/Api.Test/Controllers/ServiceAccountsControllerTests.cs +++ b/test/Api.Test/Controllers/ServiceAccountsControllerTests.cs @@ -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 sutProvider, Guid id) { + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); var result = await sutProvider.Sut.GetServiceAccountsByOrganizationAsync(id); await sutProvider.GetDependency().Received(1) - .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id))); + .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), Arg.Any(), Arg.Any()); Assert.Empty(result.Data); } @@ -33,12 +39,15 @@ public class ServiceAccountsControllerTests [BitAutoData] public async void GetServiceAccountsByOrganization_Success(SutProvider sutProvider, ServiceAccount resultServiceAccount) { - sutProvider.GetDependency().GetManyByOrganizationIdAsync(default).ReturnsForAnyArgs(new List() { resultServiceAccount }); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + sutProvider.GetDependency().GetManyByOrganizationIdAsync(default, default, default).ReturnsForAnyArgs(new List() { resultServiceAccount }); var result = await sutProvider.Sut.GetServiceAccountsByOrganizationAsync(resultServiceAccount.OrganizationId); await sutProvider.GetDependency().Received(1) - .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultServiceAccount.OrganizationId))); + .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultServiceAccount.OrganizationId)), Arg.Any(), Arg.Any()); + Assert.NotEmpty(result.Data); + Assert.Single(result.Data); } @@ -46,8 +55,8 @@ public class ServiceAccountsControllerTests [BitAutoData] public async void CreateServiceAccount_Success(SutProvider sutProvider, ServiceAccountCreateRequestModel data, Guid organizationId) { + sutProvider.GetDependency().OrganizationUser(default).ReturnsForAnyArgs(true); var resultServiceAccount = data.ToServiceAccount(organizationId); - sutProvider.GetDependency().CreateAsync(default).ReturnsForAnyArgs(resultServiceAccount); var result = await sutProvider.Sut.CreateServiceAccountAsync(organizationId, data); @@ -55,28 +64,113 @@ public class ServiceAccountsControllerTests .CreateAsync(Arg.Any()); } + [Theory] + [BitAutoData] + public async void CreateServiceAccount_NotOrgUser_Throws(SutProvider sutProvider, ServiceAccountCreateRequestModel data, Guid organizationId) + { + sutProvider.GetDependency().OrganizationUser(default).ReturnsForAnyArgs(false); + var resultServiceAccount = data.ToServiceAccount(organizationId); + sutProvider.GetDependency().CreateAsync(default).ReturnsForAnyArgs(resultServiceAccount); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateServiceAccountAsync(organizationId, data)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any()); + } + [Theory] [BitAutoData] public async void UpdateServiceAccount_Success(SutProvider sutProvider, ServiceAccountUpdateRequestModel data, Guid serviceAccountId) { + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); var resultServiceAccount = data.ToServiceAccount(serviceAccountId); - sutProvider.GetDependency().UpdateAsync(default).ReturnsForAnyArgs(resultServiceAccount); + sutProvider.GetDependency().UpdateAsync(default, default).ReturnsForAnyArgs(resultServiceAccount); var result = await sutProvider.Sut.UpdateServiceAccountAsync(serviceAccountId, data); await sutProvider.GetDependency().Received(1) - .UpdateAsync(Arg.Any()); + .UpdateAsync(Arg.Any(), Arg.Any()); } [Theory] [BitAutoData] public async void CreateAccessToken_Success(SutProvider sutProvider, AccessTokenCreateRequestModel data, Guid serviceAccountId) { + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); var resultAccessToken = data.ToApiKey(serviceAccountId); - sutProvider.GetDependency().CreateAsync(default).ReturnsForAnyArgs(resultAccessToken); + sutProvider.GetDependency().CreateAsync(default, default).ReturnsForAnyArgs(resultAccessToken); var result = await sutProvider.Sut.CreateAccessTokenAsync(serviceAccountId, data); await sutProvider.GetDependency().Received(1) - .CreateAsync(Arg.Any()); + .CreateAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void GetAccessTokens_NoServiceAccount_ThrowsNotFound(SutProvider sutProvider, Guid serviceAccountId) + { + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + sutProvider.GetDependency().GetByIdAsync(default).ReturnsNull(); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetAccessTokens(serviceAccountId)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByServiceAccountIdAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void GetAccessTokens_Admin_Success(SutProvider sutProvider, ServiceAccount data, Guid userId, ICollection resultApiKeys) + { + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().OrganizationAdmin(data.OrganizationId).Returns(true); + sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); + foreach (var apiKey in resultApiKeys) + { + apiKey.Scope = "[\"api.secrets\"]"; + } + sutProvider.GetDependency().GetManyByServiceAccountIdAsync(default).ReturnsForAnyArgs(resultApiKeys); + + var result = await sutProvider.Sut.GetAccessTokens(data.Id); + await sutProvider.GetDependency().Received(1).GetManyByServiceAccountIdAsync(Arg.Any()); + Assert.NotEmpty(result.Data); + Assert.Equal(resultApiKeys.Count, result.Data.Count()); + } + + [Theory] + [BitAutoData] + public async void GetAccessTokens_User_Success(SutProvider sutProvider, ServiceAccount data, Guid userId, ICollection resultApiKeys) + { + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().OrganizationAdmin(data.OrganizationId).Returns(false); + sutProvider.GetDependency().UserHasReadAccessToServiceAccount(default, default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); + foreach (var apiKey in resultApiKeys) + { + apiKey.Scope = "[\"api.secrets\"]"; + } + sutProvider.GetDependency().GetManyByServiceAccountIdAsync(default).ReturnsForAnyArgs(resultApiKeys); + + var result = await sutProvider.Sut.GetAccessTokens(data.Id); + await sutProvider.GetDependency().Received(1).GetManyByServiceAccountIdAsync(Arg.Any()); + Assert.NotEmpty(result.Data); + Assert.Equal(resultApiKeys.Count, result.Data.Count()); + } + + [Theory] + [BitAutoData] + public async void GetAccessTokens_UserNoAccess_ThrowsNotFound(SutProvider sutProvider, ServiceAccount data, Guid userId, ICollection resultApiKeys) + { + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().OrganizationAdmin(data.OrganizationId).Returns(false); + sutProvider.GetDependency().UserHasReadAccessToServiceAccount(default, default).ReturnsForAnyArgs(false); + sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); + foreach (var apiKey in resultApiKeys) + { + apiKey.Scope = "[\"api.secrets\"]"; + } + sutProvider.GetDependency().GetManyByServiceAccountIdAsync(default).ReturnsForAnyArgs(resultApiKeys); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetAccessTokens(data.Id)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetManyByServiceAccountIdAsync(Arg.Any()); } }