mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
SM-1146: Secrets Manager total counts (#4200)
* SM-1146: SM Organization Counts for Projects, Secrets, Machine Accounts * SM-1146: Project total counts * SM-1146: models object renames * SM-1146: Service Account total counts * SM-1146: Unit test coverage for counts controller * SM-1146: Counts controller simplification, UT update * SM-1146: Service Account total counts from Service Account auth user * SM-1146: Integration Tests for total counts controller * SM-1146: Explicitly denying access for Service Accounts * SM-1146: Fix broken ProjectsController integration test * SM-1146: Integration tests for counts controller * SM-1146: Explicitly denying access for Service Accounts cleanup * SM-1146: Test cleanup * SM-1146: PR review comments fix * SM-1146: People, Service Accounts positive count on write access * Update bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
This commit is contained in:
119
src/Api/SecretsManager/Controllers/CountsController.cs
Normal file
119
src/Api/SecretsManager/Controllers/CountsController.cs
Normal file
@ -0,0 +1,119 @@
|
||||
#nullable enable
|
||||
using Bit.Api.SecretsManager.Models.Response;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.SecretsManager.Controllers;
|
||||
|
||||
[Authorize("secrets")]
|
||||
public class CountsController : Controller
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IAccessClientQuery _accessClientQuery;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||
|
||||
public CountsController(
|
||||
ICurrentContext currentContext,
|
||||
IAccessClientQuery accessClientQuery,
|
||||
IProjectRepository projectRepository,
|
||||
ISecretRepository secretRepository,
|
||||
IServiceAccountRepository serviceAccountRepository)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_accessClientQuery = accessClientQuery;
|
||||
_projectRepository = projectRepository;
|
||||
_secretRepository = secretRepository;
|
||||
_serviceAccountRepository = serviceAccountRepository;
|
||||
}
|
||||
|
||||
[HttpGet("organizations/{organizationId}/sm-counts")]
|
||||
public async Task<OrganizationCountsResponseModel> GetByOrganizationAsync([FromRoute] Guid organizationId)
|
||||
{
|
||||
var (accessType, userId) = await GetAccessClientAsync(organizationId);
|
||||
|
||||
var projectsCountTask = _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId,
|
||||
userId, accessType);
|
||||
|
||||
var secretsCountTask = _secretRepository.GetSecretsCountByOrganizationIdAsync(organizationId,
|
||||
userId, accessType);
|
||||
|
||||
var serviceAccountsCountsTask = _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync(
|
||||
organizationId, userId, accessType);
|
||||
|
||||
var counts = await Task.WhenAll(projectsCountTask, secretsCountTask, serviceAccountsCountsTask);
|
||||
|
||||
return new OrganizationCountsResponseModel
|
||||
{
|
||||
Projects = counts[0],
|
||||
Secrets = counts[1],
|
||||
ServiceAccounts = counts[2]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("projects/{projectId}/sm-counts")]
|
||||
public async Task<ProjectCountsResponseModel> GetByProjectAsync([FromRoute] Guid projectId)
|
||||
{
|
||||
var project = await _projectRepository.GetByIdAsync(projectId);
|
||||
if (project == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var (accessType, userId) = await GetAccessClientAsync(project.OrganizationId);
|
||||
|
||||
var projectsCounts = await _projectRepository.GetProjectCountsByIdAsync(projectId, userId, accessType);
|
||||
|
||||
return new ProjectCountsResponseModel
|
||||
{
|
||||
Secrets = projectsCounts.Secrets,
|
||||
People = projectsCounts.People,
|
||||
ServiceAccounts = projectsCounts.ServiceAccounts
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("service-accounts/{serviceAccountId}/sm-counts")]
|
||||
public async Task<ServiceAccountCountsResponseModel> GetByServiceAccountAsync([FromRoute] Guid serviceAccountId)
|
||||
{
|
||||
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(serviceAccountId);
|
||||
if (serviceAccount == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var (accessType, userId) = await GetAccessClientAsync(serviceAccount.OrganizationId);
|
||||
|
||||
var serviceAccountCounts =
|
||||
await _serviceAccountRepository.GetServiceAccountCountsByIdAsync(serviceAccountId, userId, accessType);
|
||||
|
||||
return new ServiceAccountCountsResponseModel
|
||||
{
|
||||
Projects = serviceAccountCounts.Projects,
|
||||
People = serviceAccountCounts.People,
|
||||
AccessTokens = serviceAccountCounts.AccessTokens
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<(AccessClientType, Guid)> GetAccessClientAsync(Guid organizationId)
|
||||
{
|
||||
if (!_currentContext.AccessSecretsManager(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var (accessType, userId) = await _accessClientQuery.GetAccessClientAsync(User, organizationId);
|
||||
if (accessType == AccessClientType.ServiceAccount)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return (accessType, userId);
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.SecretsManager.Models.Response;
|
||||
|
||||
public class OrganizationCountsResponseModel() : ResponseModel(_objectName)
|
||||
{
|
||||
private const string _objectName = "organizationCounts";
|
||||
|
||||
public int Projects { get; set; }
|
||||
|
||||
public int Secrets { get; set; }
|
||||
|
||||
public int ServiceAccounts { get; set; }
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.SecretsManager.Models.Response;
|
||||
|
||||
public class ProjectCountsResponseModel() : ResponseModel(_objectName)
|
||||
{
|
||||
private const string _objectName = "projectCounts";
|
||||
|
||||
public int Secrets { get; set; }
|
||||
|
||||
public int People { get; set; }
|
||||
|
||||
public int ServiceAccounts { get; set; }
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.SecretsManager.Models.Response;
|
||||
|
||||
public class ServiceAccountCountsResponseModel() : ResponseModel(_objectName)
|
||||
{
|
||||
private const string _objectName = "serviceAccountCounts";
|
||||
|
||||
public int Projects { get; set; }
|
||||
|
||||
public int People { get; set; }
|
||||
|
||||
public int AccessTokens { get; set; }
|
||||
}
|
10
src/Core/SecretsManager/Models/Data/ProjectCounts.cs
Normal file
10
src/Core/SecretsManager/Models/Data/ProjectCounts.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Bit.Core.SecretsManager.Models.Data;
|
||||
|
||||
public class ProjectCounts
|
||||
{
|
||||
public int Secrets { get; set; }
|
||||
|
||||
public int People { get; set; }
|
||||
|
||||
public int ServiceAccounts { get; set; }
|
||||
}
|
10
src/Core/SecretsManager/Models/Data/ServiceAccountCounts.cs
Normal file
10
src/Core/SecretsManager/Models/Data/ServiceAccountCounts.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Bit.Core.SecretsManager.Models.Data;
|
||||
|
||||
public class ServiceAccountCounts
|
||||
{
|
||||
public int Projects { get; set; }
|
||||
|
||||
public int People { get; set; }
|
||||
|
||||
public int AccessTokens { get; set; }
|
||||
}
|
@ -17,6 +17,8 @@ public interface IProjectRepository
|
||||
Task<(bool Read, bool Write)> AccessToProjectAsync(Guid id, Guid userId, AccessClientType accessType);
|
||||
Task<bool> ProjectsAreInOrganization(List<Guid> projectIds, Guid organizationId);
|
||||
Task<int> GetProjectCountByOrganizationIdAsync(Guid organizationId);
|
||||
Task<int> GetProjectCountByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);
|
||||
Task<ProjectCounts> GetProjectCountsByIdAsync(Guid projectId, Guid userId, AccessClientType accessType);
|
||||
Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToProjectsAsync(IEnumerable<Guid> projectIds, Guid userId,
|
||||
AccessClientType accessType);
|
||||
}
|
||||
|
@ -24,4 +24,5 @@ public interface ISecretRepository
|
||||
Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToSecretsAsync(IEnumerable<Guid> ids, Guid userId, AccessClientType accessType);
|
||||
Task EmptyTrash(DateTime nowTime, uint deleteAfterThisNumberOfDays);
|
||||
Task<int> GetSecretsCountByOrganizationIdAsync(Guid organizationId);
|
||||
Task<int> GetSecretsCountByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);
|
||||
}
|
||||
|
@ -17,6 +17,9 @@ public interface IServiceAccountRepository
|
||||
Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToServiceAccountsAsync(IEnumerable<Guid> ids, Guid userId,
|
||||
AccessClientType accessType);
|
||||
Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId);
|
||||
Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);
|
||||
Task<ServiceAccountCounts> GetServiceAccountCountsByIdAsync(Guid serviceAccountId, Guid userId, AccessClientType accessType);
|
||||
|
||||
Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(Guid organizationId, Guid userId, AccessClientType accessType);
|
||||
Task<bool> ServiceAccountsAreInOrganizationAsync(List<Guid> serviceAccountIds, Guid organizationId);
|
||||
}
|
||||
|
@ -63,6 +63,17 @@ public class NoopProjectRepository : IProjectRepository
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task<int> GetProjectCountByOrganizationIdAsync(Guid organizationId, Guid userId,
|
||||
AccessClientType accessType)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task<ProjectCounts> GetProjectCountsByIdAsync(Guid projectId, Guid userId, AccessClientType accessType)
|
||||
{
|
||||
return Task.FromResult(null as ProjectCounts);
|
||||
}
|
||||
|
||||
public Task<Dictionary<Guid, (bool Read, bool Write)>> AccessToProjectsAsync(IEnumerable<Guid> projectIds,
|
||||
Guid userId, AccessClientType accessType)
|
||||
{
|
||||
|
@ -96,4 +96,10 @@ public class NoopSecretRepository : ISecretRepository
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task<int> GetSecretsCountByOrganizationIdAsync(Guid organizationId, Guid userId,
|
||||
AccessClientType accessType)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,18 @@ public class NoopServiceAccountRepository : IServiceAccountRepository
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task<int> GetServiceAccountCountByOrganizationIdAsync(Guid organizationId, Guid userId,
|
||||
AccessClientType accessType)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task<ServiceAccountCounts> GetServiceAccountCountsByIdAsync(Guid serviceAccountId, Guid userId,
|
||||
AccessClientType accessType)
|
||||
{
|
||||
return Task.FromResult(null as ServiceAccountCounts);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(
|
||||
Guid organizationId, Guid userId, AccessClientType accessType)
|
||||
{
|
||||
|
@ -8,6 +8,8 @@ public class ServiceAccount : Core.SecretsManager.Entities.ServiceAccount
|
||||
public virtual Organization Organization { get; set; }
|
||||
public virtual ICollection<GroupServiceAccountAccessPolicy> GroupAccessPolicies { get; set; }
|
||||
public virtual ICollection<UserServiceAccountAccessPolicy> UserAccessPolicies { get; set; }
|
||||
public virtual ICollection<ServiceAccountProjectAccessPolicy> ProjectAccessPolicies { get; set; }
|
||||
public virtual ICollection<ApiKey> ApiKeys { get; set; }
|
||||
}
|
||||
|
||||
public class ServiceAccountMapperProfile : Profile
|
||||
|
Reference in New Issue
Block a user