Skip to content

Commit

Permalink
Merge branch 'master' into feature/sm-billing-round-2
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/Api/SecretsManager/Controllers/ServiceAccountsController.cs
  • Loading branch information
r-tome committed Aug 3, 2023
2 parents b7ebd7e + 78588d0 commit 5b0a701
Show file tree
Hide file tree
Showing 19 changed files with 253 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Repositories;

namespace Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;

public class ServiceAccountSecretsDetailsQuery : IServiceAccountSecretsDetailsQuery
{
private readonly IServiceAccountRepository _serviceAccountRepository;

public ServiceAccountSecretsDetailsQuery(IServiceAccountRepository serviceAccountRepository)
{
_serviceAccountRepository = serviceAccountRepository;
}

public async Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdAsync(
Guid organizationId, Guid userId, AccessClientType accessClient, bool includeAccessToSecrets)
{
if (includeAccessToSecrets)
{
return await _serviceAccountRepository.GetManyByOrganizationIdWithSecretsDetailsAsync(organizationId,
userId,
accessClient);
}

var serviceAccounts =
await _serviceAccountRepository.GetManyByOrganizationIdAsync(organizationId, userId, accessClient);

return serviceAccounts.Select(sa => new ServiceAccountSecretsDetails
{
ServiceAccount = sa,
AccessToSecrets = 0,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public static void AddSecretsManagerServices(this IServiceCollection services)
services.AddScoped<IAuthorizationHandler, ServiceAccountAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, AccessPolicyAuthorizationHandler>();
services.AddScoped<IAccessClientQuery, AccessClientQuery>();
services.AddScoped<IServiceAccountSecretsDetailsQuery, ServiceAccountSecretsDetailsQuery>();
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
services.AddScoped<IDeleteSecretCommand, DeleteSecretCommand>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Linq.Expressions;
using AutoMapper;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
Expand Down Expand Up @@ -140,6 +141,44 @@ public async Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organiza
}
}

public async Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(
Guid organizationId, Guid userId, AccessClientType accessType)
{
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = from sa in dbContext.ServiceAccount
join ap in dbContext.ServiceAccountProjectAccessPolicy
on sa.Id equals ap.ServiceAccountId into grouping
from ap in grouping.DefaultIfEmpty()
where sa.OrganizationId == organizationId
select new
{
ServiceAccount = sa,
AccessToSecrets = ap.GrantedProject.Secrets.Count(s => s.DeletedDate == null)
};

query = accessType switch
{
AccessClientType.NoAccessCheck => query,
AccessClientType.User => query.Where(c =>
c.ServiceAccount.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
c.ServiceAccount.GroupAccessPolicies.Any(ap =>
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read))),
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),
};

var results = (await query.ToListAsync())
.GroupBy(g => g.ServiceAccount)
.Select(g =>
new ServiceAccountSecretsDetails
{
ServiceAccount = Mapper.Map<Core.SecretsManager.Entities.ServiceAccount>(g.Key),
AccessToSecrets = g.Sum(x => x.AccessToSecrets),
}).OrderBy(c => c.ServiceAccount.RevisionDate).ToList();

return results;
}

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));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using NSubstitute;
using Xunit;

namespace Bit.Commercial.Core.Test.SecretsManager.Queries.ServiceAccounts;

[SutProviderCustomize]
public class ServiceAccountSecretsDetailsQueryTests
{
[Theory]
[BitAutoData(false)]
[BitAutoData(true)]
public async Task GetManyByOrganizationId_CallsDifferentRepoMethods(
bool includeAccessToSecrets,
SutProvider<ServiceAccountSecretsDetailsQuery> sutProvider,
Guid organizationId,
Guid userId,
AccessClientType accessClient,
ServiceAccount mockSa,
ServiceAccountSecretsDetails mockSaDetails)
{
sutProvider.GetDependency<IServiceAccountRepository>().GetManyByOrganizationIdAsync(default, default, default)
.ReturnsForAnyArgs(new List<ServiceAccount> { mockSa });

sutProvider.GetDependency<IServiceAccountRepository>().GetManyByOrganizationIdWithSecretsDetailsAsync(default, default, default)
.ReturnsForAnyArgs(new List<ServiceAccountSecretsDetails> { mockSaDetails });


var result = await sutProvider.Sut.GetManyByOrganizationIdAsync(organizationId, userId, accessClient, includeAccessToSecrets);

if (includeAccessToSecrets)
{
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)
.GetManyByOrganizationIdWithSecretsDetailsAsync(Arg.Is(AssertHelper.AssertPropertyEqual(mockSaDetails.ServiceAccount.OrganizationId)),
Arg.Any<Guid>(), Arg.Any<AccessClientType>());
}
else
{
await sutProvider.GetDependency<IServiceAccountRepository>().Received(1)
.GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(mockSa.OrganizationId)),
Arg.Any<Guid>(), Arg.Any<AccessClientType>());
Assert.Equal(0, result.First().AccessToSecrets);
}
}
}
17 changes: 10 additions & 7 deletions src/Api/SecretsManager/Controllers/ServiceAccountsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public class ServiceAccountsController : Controller
private readonly IOrganizationRepository _organizationRepository;
private readonly ICountNewServiceAccountSlotsRequiredQuery _countNewServiceAccountSlotsRequiredQuery;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly IServiceAccountSecretsDetailsQuery _serviceAccountSecretsDetailsQuery;
private readonly ICreateAccessTokenCommand _createAccessTokenCommand;
private readonly ICreateServiceAccountCommand _createServiceAccountCommand;
private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand;
Expand All @@ -47,6 +48,7 @@ public ServiceAccountsController(
IOrganizationRepository organizationRepository,
ICountNewServiceAccountSlotsRequiredQuery countNewServiceAccountSlotsRequiredQuery,
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
IServiceAccountSecretsDetailsQuery serviceAccountSecretsDetailsQuery,
ICreateAccessTokenCommand createAccessTokenCommand,
ICreateServiceAccountCommand createServiceAccountCommand,
IUpdateServiceAccountCommand updateServiceAccountCommand,
Expand All @@ -60,6 +62,7 @@ public ServiceAccountsController(
_apiKeyRepository = apiKeyRepository;
_organizationRepository = organizationRepository;
_countNewServiceAccountSlotsRequiredQuery = countNewServiceAccountSlotsRequiredQuery;
_serviceAccountSecretsDetailsQuery = serviceAccountSecretsDetailsQuery;
_createServiceAccountCommand = createServiceAccountCommand;
_updateServiceAccountCommand = updateServiceAccountCommand;
_deleteServiceAccountsCommand = deleteServiceAccountsCommand;
Expand All @@ -69,8 +72,8 @@ public ServiceAccountsController(
}

[HttpGet("/organizations/{organizationId}/service-accounts")]
public async Task<ListResponseModel<ServiceAccountResponseModel>> ListByOrganizationAsync(
[FromRoute] Guid organizationId)
public async Task<ListResponseModel<ServiceAccountSecretsDetailsResponseModel>> ListByOrganizationAsync(
[FromRoute] Guid organizationId, [FromQuery] bool includeAccessToSecrets = false)
{
if (!_currentContext.AccessSecretsManager(organizationId))
{
Expand All @@ -81,11 +84,11 @@ public async Task<ListResponseModel<ServiceAccountResponseModel>> ListByOrganiza
var orgAdmin = await _currentContext.OrganizationAdmin(organizationId);
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.ClientType, orgAdmin);

var serviceAccounts =
await _serviceAccountRepository.GetManyByOrganizationIdAsync(organizationId, userId, accessClient);

var responses = serviceAccounts.Select(serviceAccount => new ServiceAccountResponseModel(serviceAccount));
return new ListResponseModel<ServiceAccountResponseModel>(responses);
var results =
await _serviceAccountSecretsDetailsQuery.GetManyByOrganizationIdAsync(organizationId, userId, accessClient,
includeAccessToSecrets);
var responses = results.Select(r => new ServiceAccountSecretsDetailsResponseModel(r));
return new ListResponseModel<ServiceAccountSecretsDetailsResponseModel>(responses);
}

[HttpGet("{id}")]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Bit.Core.Models.Api;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;

namespace Bit.Api.SecretsManager.Models.Response;

Expand Down Expand Up @@ -35,3 +36,18 @@ public ServiceAccountResponseModel() : base(_objectName)

public DateTime RevisionDate { get; set; }
}

public class ServiceAccountSecretsDetailsResponseModel : ServiceAccountResponseModel
{
public ServiceAccountSecretsDetailsResponseModel(ServiceAccountSecretsDetails serviceAccountDetails) : base(serviceAccountDetails.ServiceAccount)
{
if (serviceAccountDetails == null)
{
throw new ArgumentNullException(nameof(serviceAccountDetails));
}

AccessToSecrets = serviceAccountDetails.AccessToSecrets;
}

public int AccessToSecrets { get; set; }
}
2 changes: 1 addition & 1 deletion src/Core/Repositories/IOrganizationUserRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ Task<OrganizationUserOrganizationDetails> GetDetailsByUserAsync(Guid userId, Gui
Task<IEnumerable<OrganizationUserUserDetails>> GetManyByMinimumRoleAsync(Guid organizationId, OrganizationUserType minRole);
Task RevokeAsync(Guid id);
Task RestoreAsync(Guid id, OrganizationUserStatusType status);
Task<IEnumerable<OrganizationUserPolicyDetails>> GetByUserIdWithPolicyDetailsAsync(Guid userId, PolicyType policyType);
Task<IEnumerable<OrganizationUserPolicyDetails>> GetByUserIdWithPolicyDetailsAsync(Guid userId);
Task<int> GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Bit.Core.SecretsManager.Entities;

namespace Bit.Core.SecretsManager.Models.Data;

public class ServiceAccountSecretsDetails
{
public ServiceAccount ServiceAccount { get; set; }
public int AccessToSecrets { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Models.Data;

namespace Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;

public interface IServiceAccountSecretsDetailsQuery
{
public Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdAsync(
Guid organizationId, Guid userId, AccessClientType accessClient, bool includeAccessToSecrets);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;

namespace Bit.Core.SecretsManager.Repositories;

Expand All @@ -16,4 +17,5 @@ public interface IServiceAccountRepository
Task<IEnumerable<ServiceAccount>> GetManyByOrganizationIdWriteAccessAsync(Guid organizationId, Guid userId, AccessClientType accessType);
Task<(bool Read, bool Write)> AccessToServiceAccountAsync(Guid id, Guid userId, AccessClientType accessType);
Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId);
Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(Guid organizationId, Guid userId, AccessClientType accessType);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Models.Data;

namespace Bit.Core.SecretsManager.Repositories.Noop;

Expand Down Expand Up @@ -56,4 +57,6 @@ public Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId
{
return Task.FromResult(0);
}

public Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(Guid organizationId, Guid userId, AccessClientType accessType) => throw new NotImplementedException();
}
17 changes: 13 additions & 4 deletions src/Core/Services/Implementations/PolicyService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class PolicyService : IPolicyService
private readonly IMailService _mailService;
private readonly GlobalSettings _globalSettings;

private IEnumerable<OrganizationUserPolicyDetails> _cachedOrganizationUserPolicyDetails;

public PolicyService(
IEventService eventService,
IOrganizationRepository organizationRepository,
Expand Down Expand Up @@ -194,18 +196,25 @@ public async Task<bool> AnyPoliciesApplicableToUserAsync(Guid userId, PolicyType
return result.Any();
}

private async Task<IEnumerable<OrganizationUserPolicyDetails>> QueryOrganizationUserPolicyDetailsAsync(Guid userId, PolicyType policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted)
private async Task<IEnumerable<OrganizationUserPolicyDetails>> QueryOrganizationUserPolicyDetailsAsync(Guid userId, PolicyType? policyType, OrganizationUserStatusType minStatus = OrganizationUserStatusType.Accepted)
{
var organizationUserPolicyDetails = await _organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(userId, policyType);
// Check if the cached policies are available
if (_cachedOrganizationUserPolicyDetails == null)
{
// Cached policies not available, retrieve from the repository
_cachedOrganizationUserPolicyDetails = await _organizationUserRepository.GetByUserIdWithPolicyDetailsAsync(userId);
}

var excludedUserTypes = GetUserTypesExcludedFromPolicy(policyType);
return organizationUserPolicyDetails.Where(o =>
return _cachedOrganizationUserPolicyDetails.Where(o =>
(policyType == null || o.PolicyType == policyType) &&
o.PolicyEnabled &&
!excludedUserTypes.Contains(o.OrganizationUserType) &&
o.OrganizationUserStatus >= minStatus &&
!o.IsProvider);
}

private OrganizationUserType[] GetUserTypesExcludedFromPolicy(PolicyType policyType)
private OrganizationUserType[] GetUserTypesExcludedFromPolicy(PolicyType? policyType)
{
switch (policyType)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,13 +505,13 @@ public async Task RestoreAsync(Guid id, OrganizationUserStatusType status)
}
}

public async Task<IEnumerable<OrganizationUserPolicyDetails>> GetByUserIdWithPolicyDetailsAsync(Guid userId, PolicyType policyType)
public async Task<IEnumerable<OrganizationUserPolicyDetails>> GetByUserIdWithPolicyDetailsAsync(Guid userId)
{
using (var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<OrganizationUserPolicyDetails>(
$"[{Schema}].[{Table}_ReadByUserIdWithPolicyDetails]",
new { UserId = userId, PolicyType = policyType },
new { UserId = userId },
commandType: CommandType.StoredProcedure);

return results.ToList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ public async Task RestoreAsync(Guid id, OrganizationUserStatusType status)
}
}

public async Task<IEnumerable<OrganizationUserPolicyDetails>> GetByUserIdWithPolicyDetailsAsync(Guid userId, PolicyType policyType)
public async Task<IEnumerable<OrganizationUserPolicyDetails>> GetByUserIdWithPolicyDetailsAsync(Guid userId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
Expand All @@ -604,8 +604,7 @@ on pu.ProviderId equals po.ProviderId
join ou in dbContext.OrganizationUsers
on p.OrganizationId equals ou.OrganizationId
let email = dbContext.Users.Find(userId).Email // Invited orgUsers do not have a UserId associated with them, so we have to match up their email
where p.Type == policyType &&
(ou.UserId == userId || ou.Email == email)
where ou.UserId == userId || ou.Email == email
select new OrganizationUserPolicyDetails
{
OrganizationUserId = ou.Id,
Expand Down
Loading

0 comments on commit 5b0a701

Please sign in to comment.