mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 05:00:19 -05:00
[SM-654] Add support for direct secret permissions at the repo layer (#4156)
* calculate direct secret permissions at the repo layer * Add integration tests for service account secret access count
This commit is contained in:
parent
7f496e7399
commit
0e6e461602
@ -289,35 +289,13 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
|||||||
|
|
||||||
public async Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType)
|
public async Task<(bool Read, bool Write)> AccessToSecretAsync(Guid id, Guid userId, AccessClientType accessType)
|
||||||
{
|
{
|
||||||
using var scope = ServiceScopeFactory.CreateScope();
|
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
|
||||||
var secret = dbContext.Secret
|
var secret = dbContext.Secret
|
||||||
.Where(s => s.Id == id);
|
.Where(s => s.Id == id);
|
||||||
|
|
||||||
var query = accessType switch
|
var query = BuildSecretAccessQuery(secret, userId, accessType);
|
||||||
{
|
|
||||||
AccessClientType.NoAccessCheck => secret.Select(_ => new { Read = true, Write = true }),
|
|
||||||
AccessClientType.User => secret.Select(s => new
|
|
||||||
{
|
|
||||||
Read = s.Projects.Any(p =>
|
|
||||||
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
|
||||||
p.GroupAccessPolicies.Any(ap =>
|
|
||||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read))),
|
|
||||||
Write = s.Projects.Any(p =>
|
|
||||||
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
|
|
||||||
p.GroupAccessPolicies.Any(ap =>
|
|
||||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write))),
|
|
||||||
}),
|
|
||||||
AccessClientType.ServiceAccount => secret.Select(s => new
|
|
||||||
{
|
|
||||||
Read = s.Projects.Any(p =>
|
|
||||||
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Read)),
|
|
||||||
Write = s.Projects.Any(p =>
|
|
||||||
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == userId && ap.Write)),
|
|
||||||
}),
|
|
||||||
_ => secret.Select(_ => new { Read = false, Write = false }),
|
|
||||||
};
|
|
||||||
|
|
||||||
var policy = await query.FirstOrDefaultAsync();
|
var policy = await query.FirstOrDefaultAsync();
|
||||||
|
|
||||||
@ -361,19 +339,27 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
|||||||
private Expression<Func<Secret, SecretPermissionDetails>> SecretToPermissionsUser(Guid userId, bool read) =>
|
private Expression<Func<Secret, SecretPermissionDetails>> SecretToPermissionsUser(Guid userId, bool read) =>
|
||||||
s => new SecretPermissionDetails
|
s => new SecretPermissionDetails
|
||||||
{
|
{
|
||||||
Secret = Mapper.Map<Bit.Core.SecretsManager.Entities.Secret>(s),
|
Secret = Mapper.Map<Core.SecretsManager.Entities.Secret>(s),
|
||||||
Read = read,
|
Read = read,
|
||||||
Write = s.Projects.Any(p =>
|
Write =
|
||||||
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
|
s.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
|
||||||
p.GroupAccessPolicies.Any(ap =>
|
s.GroupAccessPolicies.Any(ap =>
|
||||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write))),
|
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)) ||
|
||||||
|
s.Projects.Any(p =>
|
||||||
|
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
|
||||||
|
p.GroupAccessPolicies.Any(ap =>
|
||||||
|
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write)))
|
||||||
};
|
};
|
||||||
|
|
||||||
private static Expression<Func<Secret, bool>> ServiceAccountHasReadAccessToSecret(Guid serviceAccountId) => s =>
|
private static Expression<Func<Secret, bool>> ServiceAccountHasReadAccessToSecret(Guid serviceAccountId) => s =>
|
||||||
|
s.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == serviceAccountId && ap.Read) ||
|
||||||
s.Projects.Any(p =>
|
s.Projects.Any(p =>
|
||||||
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Read));
|
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccount.Id == serviceAccountId && ap.Read));
|
||||||
|
|
||||||
private static Expression<Func<Secret, bool>> UserHasReadAccessToSecret(Guid userId) => s =>
|
private static Expression<Func<Secret, bool>> UserHasReadAccessToSecret(Guid userId) => s =>
|
||||||
|
s.UserAccessPolicies.Any(ap => ap.OrganizationUser.UserId == userId && ap.Read) ||
|
||||||
|
s.GroupAccessPolicies.Any(ap =>
|
||||||
|
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.UserId == userId && ap.Read)) ||
|
||||||
s.Projects.Any(p =>
|
s.Projects.Any(p =>
|
||||||
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.UserId == userId && ap.Read) ||
|
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.UserId == userId && ap.Read) ||
|
||||||
p.GroupAccessPolicies.Any(ap =>
|
p.GroupAccessPolicies.Any(ap =>
|
||||||
@ -434,4 +420,40 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
|||||||
.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.RevisionDate, utcNow));
|
.ExecuteUpdateAsync(setters => setters.SetProperty(b => b.RevisionDate, utcNow));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IQueryable<SecretAccess> BuildSecretAccessQuery(IQueryable<Secret> secrets, Guid accessClientId,
|
||||||
|
AccessClientType accessType) =>
|
||||||
|
accessType switch
|
||||||
|
{
|
||||||
|
AccessClientType.NoAccessCheck => secrets.Select(s => new SecretAccess(s.Id, true, true)),
|
||||||
|
AccessClientType.User => secrets.Select(s => new SecretAccess(
|
||||||
|
s.Id,
|
||||||
|
s.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == accessClientId && ap.Read) ||
|
||||||
|
s.GroupAccessPolicies.Any(ap =>
|
||||||
|
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == accessClientId && ap.Read)) ||
|
||||||
|
s.Projects.Any(p =>
|
||||||
|
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == accessClientId && ap.Read) ||
|
||||||
|
p.GroupAccessPolicies.Any(ap =>
|
||||||
|
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == accessClientId && ap.Read))),
|
||||||
|
s.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == accessClientId && ap.Write) ||
|
||||||
|
s.GroupAccessPolicies.Any(ap =>
|
||||||
|
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == accessClientId && ap.Write)) ||
|
||||||
|
s.Projects.Any(p =>
|
||||||
|
p.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == accessClientId && ap.Write) ||
|
||||||
|
p.GroupAccessPolicies.Any(ap =>
|
||||||
|
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == accessClientId && ap.Write)))
|
||||||
|
)),
|
||||||
|
AccessClientType.ServiceAccount => secrets.Select(s => new SecretAccess(
|
||||||
|
s.Id,
|
||||||
|
s.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == accessClientId && ap.Read) ||
|
||||||
|
s.Projects.Any(p =>
|
||||||
|
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == accessClientId && ap.Read)),
|
||||||
|
s.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == accessClientId && ap.Write) ||
|
||||||
|
s.Projects.Any(p =>
|
||||||
|
p.ServiceAccountAccessPolicies.Any(ap => ap.ServiceAccountId == accessClientId && ap.Write))
|
||||||
|
)),
|
||||||
|
_ => secrets.Select(s => new SecretAccess(s.Id, false, false))
|
||||||
|
};
|
||||||
|
|
||||||
|
private record SecretAccess(Guid Id, bool Read, bool Write);
|
||||||
}
|
}
|
||||||
|
@ -135,38 +135,37 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(
|
public async Task<IEnumerable<ServiceAccountSecretsDetails>> GetManyByOrganizationIdWithSecretsDetailsAsync(
|
||||||
Guid organizationId, Guid userId, AccessClientType accessType)
|
Guid organizationId, Guid userId, AccessClientType accessType)
|
||||||
{
|
{
|
||||||
using var scope = ServiceScopeFactory.CreateScope();
|
await using var scope = ServiceScopeFactory.CreateAsyncScope();
|
||||||
var dbContext = GetDatabaseContext(scope);
|
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
|
var serviceAccountQuery = dbContext.ServiceAccount.Where(c => c.OrganizationId == organizationId);
|
||||||
|
serviceAccountQuery = accessType switch
|
||||||
{
|
{
|
||||||
AccessClientType.NoAccessCheck => query,
|
AccessClientType.NoAccessCheck => serviceAccountQuery,
|
||||||
AccessClientType.User => query.Where(c =>
|
AccessClientType.User => serviceAccountQuery.Where(c =>
|
||||||
c.ServiceAccount.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
c.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Read) ||
|
||||||
c.ServiceAccount.GroupAccessPolicies.Any(ap =>
|
c.GroupAccessPolicies.Any(ap =>
|
||||||
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read))),
|
ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Read))),
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),
|
_ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null),
|
||||||
};
|
};
|
||||||
|
|
||||||
var results = (await query.ToListAsync())
|
var projectSecretsAccessQuery = BuildProjectSecretsAccessQuery(dbContext, serviceAccountQuery);
|
||||||
|
var directSecretAccessQuery = BuildDirectSecretAccessQuery(dbContext, serviceAccountQuery);
|
||||||
|
|
||||||
|
var projectSecretsAccessResults = await projectSecretsAccessQuery.ToListAsync();
|
||||||
|
var directSecretAccessResults = await directSecretAccessQuery.ToListAsync();
|
||||||
|
|
||||||
|
var applicableDirectSecretAccessResults = FilterDirectSecretAccessResults(projectSecretsAccessResults, directSecretAccessResults);
|
||||||
|
|
||||||
|
var results = projectSecretsAccessResults.Concat(applicableDirectSecretAccessResults)
|
||||||
.GroupBy(g => g.ServiceAccount)
|
.GroupBy(g => g.ServiceAccount)
|
||||||
.Select(g =>
|
.Select(g =>
|
||||||
new ServiceAccountSecretsDetails
|
new ServiceAccountSecretsDetails
|
||||||
{
|
{
|
||||||
ServiceAccount = Mapper.Map<Core.SecretsManager.Entities.ServiceAccount>(g.Key),
|
ServiceAccount = Mapper.Map<Core.SecretsManager.Entities.ServiceAccount>(g.Key),
|
||||||
AccessToSecrets = g.Sum(x => x.AccessToSecrets),
|
AccessToSecrets = g.Sum(x => x.SecretIds.Count())
|
||||||
}).OrderBy(c => c.ServiceAccount.RevisionDate).ToList();
|
}).OrderBy(c => c.ServiceAccount.RevisionDate).ToList();
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
@ -200,4 +199,46 @@ public class ServiceAccountRepository : Repository<Core.SecretsManager.Entities.
|
|||||||
private static Expression<Func<ServiceAccount, bool>> UserHasWriteAccessToServiceAccount(Guid userId) => sa =>
|
private static Expression<Func<ServiceAccount, bool>> UserHasWriteAccessToServiceAccount(Guid userId) => sa =>
|
||||||
sa.UserAccessPolicies.Any(ap => ap.OrganizationUser.User.Id == userId && ap.Write) ||
|
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));
|
sa.GroupAccessPolicies.Any(ap => ap.Group.GroupUsers.Any(gu => gu.OrganizationUser.User.Id == userId && ap.Write));
|
||||||
|
|
||||||
|
private static IQueryable<ServiceAccountSecretsAccess> BuildProjectSecretsAccessQuery(DatabaseContext dbContext,
|
||||||
|
IQueryable<ServiceAccount> serviceAccountQuery) =>
|
||||||
|
from sa in serviceAccountQuery
|
||||||
|
join ap in dbContext.ServiceAccountProjectAccessPolicy
|
||||||
|
on sa.Id equals ap.ServiceAccountId into grouping
|
||||||
|
from ap in grouping.DefaultIfEmpty()
|
||||||
|
select new ServiceAccountSecretsAccess
|
||||||
|
(
|
||||||
|
sa, ap.GrantedProject.Secrets.Where(s => s.DeletedDate == null).Select(s => s.Id)
|
||||||
|
);
|
||||||
|
|
||||||
|
private static IQueryable<ServiceAccountSecretsAccess> BuildDirectSecretAccessQuery(
|
||||||
|
DatabaseContext dbContext,
|
||||||
|
IQueryable<ServiceAccount> serviceAccountQuery) =>
|
||||||
|
from sa in serviceAccountQuery
|
||||||
|
join ap in dbContext.ServiceAccountSecretAccessPolicy
|
||||||
|
on sa.Id equals ap.ServiceAccountId into grouping
|
||||||
|
from ap in grouping.DefaultIfEmpty()
|
||||||
|
where ap.GrantedSecret.DeletedDate == null &&
|
||||||
|
ap.GrantedSecretId != null
|
||||||
|
select new ServiceAccountSecretsAccess(sa,
|
||||||
|
new List<Guid> { ap.GrantedSecretId.Value });
|
||||||
|
|
||||||
|
private static List<ServiceAccountSecretsAccess> FilterDirectSecretAccessResults(
|
||||||
|
List<ServiceAccountSecretsAccess> projectSecretsAccessResults,
|
||||||
|
List<ServiceAccountSecretsAccess> directSecretAccessResults) =>
|
||||||
|
directSecretAccessResults.Where(directSecretAccessResult =>
|
||||||
|
{
|
||||||
|
var serviceAccountId = directSecretAccessResult.ServiceAccount.Id;
|
||||||
|
var secretId = directSecretAccessResult.SecretIds.FirstOrDefault();
|
||||||
|
if (secretId == Guid.Empty)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !projectSecretsAccessResults
|
||||||
|
.Where(x => x.ServiceAccount.Id == serviceAccountId)
|
||||||
|
.Any(x => x.SecretIds.Contains(secretId));
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
private record ServiceAccountSecretsAccess(ServiceAccount ServiceAccount, IEnumerable<Guid> SecretIds);
|
||||||
}
|
}
|
||||||
|
@ -49,5 +49,9 @@ public class ServiceAccountSecretsDetailsResponseModel : ServiceAccountResponseM
|
|||||||
AccessToSecrets = serviceAccountDetails.AccessToSecrets;
|
AccessToSecrets = serviceAccountDetails.AccessToSecrets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ServiceAccountSecretsDetailsResponseModel() : base(new ServiceAccount())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public int AccessToSecrets { get; set; }
|
public int AccessToSecrets { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,8 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
|
|||||||
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
private readonly IAccessPolicyRepository _accessPolicyRepository;
|
||||||
private readonly IApiKeyRepository _apiKeyRepository;
|
private readonly IApiKeyRepository _apiKeyRepository;
|
||||||
private readonly IServiceAccountRepository _serviceAccountRepository;
|
private readonly IServiceAccountRepository _serviceAccountRepository;
|
||||||
|
private readonly IProjectRepository _projectRepository;
|
||||||
|
private readonly ISecretRepository _secretRepository;
|
||||||
|
|
||||||
private string _email = null!;
|
private string _email = null!;
|
||||||
private SecretsManagerOrganizationHelper _organizationHelper = null!;
|
private SecretsManagerOrganizationHelper _organizationHelper = null!;
|
||||||
@ -40,6 +42,8 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
|
|||||||
_serviceAccountRepository = _factory.GetService<IServiceAccountRepository>();
|
_serviceAccountRepository = _factory.GetService<IServiceAccountRepository>();
|
||||||
_accessPolicyRepository = _factory.GetService<IAccessPolicyRepository>();
|
_accessPolicyRepository = _factory.GetService<IAccessPolicyRepository>();
|
||||||
_apiKeyRepository = _factory.GetService<IApiKeyRepository>();
|
_apiKeyRepository = _factory.GetService<IApiKeyRepository>();
|
||||||
|
_secretRepository = _factory.GetService<ISecretRepository>();
|
||||||
|
_projectRepository = _factory.GetService<IProjectRepository>();
|
||||||
_loginHelper = new LoginHelper(_factory, _client);
|
_loginHelper = new LoginHelper(_factory, _client);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,51 +77,90 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
|
|||||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task ListByOrganization_Admin_Success()
|
[InlineData(PermissionType.RunAsAdmin)]
|
||||||
|
[InlineData(PermissionType.RunAsUserWithPermission)]
|
||||||
|
public async Task ListByOrganization_NoSecretAccess_Success(PermissionType permissionType)
|
||||||
{
|
{
|
||||||
var (org, _) = await _organizationHelper.Initialize(true, true, true);
|
var (orgId, serviceAccountIds) = await SetupListByOrganizationRequestAsync(permissionType);
|
||||||
await _loginHelper.LoginAsync(_email);
|
|
||||||
|
|
||||||
var serviceAccountIds = await SetupGetServiceAccountsByOrganizationAsync(org);
|
var response = await _client.GetAsync($"/organizations/{orgId}/service-accounts");
|
||||||
|
|
||||||
var response = await _client.GetAsync($"/organizations/{org.Id}/service-accounts");
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
var result = await response.Content.ReadFromJsonAsync<ListResponseModel<ServiceAccountResponseModel>>();
|
var result = await response.Content
|
||||||
|
.ReadFromJsonAsync<ListResponseModel<ServiceAccountSecretsDetailsResponseModel>>();
|
||||||
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.NotEmpty(result.Data);
|
Assert.NotEmpty(result.Data);
|
||||||
Assert.Equal(serviceAccountIds.Count, result.Data.Count());
|
Assert.Equal(serviceAccountIds.Count, result.Data.Count());
|
||||||
|
Assert.DoesNotContain(result.Data, x => x.AccessToSecrets != 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task ListByOrganization_User_Success()
|
[InlineData(PermissionType.RunAsAdmin)]
|
||||||
|
[InlineData(PermissionType.RunAsUserWithPermission)]
|
||||||
|
public async Task ListByOrganization_SecretAccess_Success(PermissionType permissionType)
|
||||||
{
|
{
|
||||||
var (org, _) = await _organizationHelper.Initialize(true, true, true);
|
var (orgId, serviceAccountIds) = await SetupListByOrganizationRequestAsync(permissionType);
|
||||||
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
|
var expectedAccess = await SetupServiceAccountSecretAccessAsync(serviceAccountIds, orgId);
|
||||||
await _loginHelper.LoginAsync(email);
|
|
||||||
|
|
||||||
var serviceAccountIds = await SetupGetServiceAccountsByOrganizationAsync(org);
|
var response = await _client.GetAsync($"/organizations/{orgId}/service-accounts?includeAccessToSecrets=true");
|
||||||
|
|
||||||
// Setup access for two
|
|
||||||
var accessPolicies = serviceAccountIds.Take(2).Select(
|
|
||||||
id => new UserServiceAccountAccessPolicy
|
|
||||||
{
|
|
||||||
OrganizationUserId = orgUser.Id,
|
|
||||||
GrantedServiceAccountId = id,
|
|
||||||
Read = true,
|
|
||||||
Write = false,
|
|
||||||
}).Cast<BaseAccessPolicy>().ToList();
|
|
||||||
|
|
||||||
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
|
|
||||||
|
|
||||||
var response = await _client.GetAsync($"/organizations/{org.Id}/service-accounts");
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
var result = await response.Content.ReadFromJsonAsync<ListResponseModel<ServiceAccountResponseModel>>();
|
var result = await response.Content
|
||||||
|
.ReadFromJsonAsync<ListResponseModel<ServiceAccountSecretsDetailsResponseModel>>();
|
||||||
|
|
||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.NotEmpty(result.Data);
|
Assert.NotEmpty(result.Data);
|
||||||
Assert.Equal(2, result.Data.Count());
|
Assert.Equal(serviceAccountIds.Count, result.Data.Count());
|
||||||
|
|
||||||
|
foreach (var item in expectedAccess)
|
||||||
|
{
|
||||||
|
var serviceAccountResult = result.Data.FirstOrDefault(x => x.Id == item.Key);
|
||||||
|
Assert.NotNull(serviceAccountResult);
|
||||||
|
Assert.Equal(item.Value, serviceAccountResult.AccessToSecrets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(false)]
|
||||||
|
[InlineData(true)]
|
||||||
|
public async Task ListByOrganization_UserPartialAccess_ReturnsServiceAccountsUserHasAccessTo(
|
||||||
|
bool includeAccessToSecrets)
|
||||||
|
{
|
||||||
|
var (orgId, serviceAccountIds) =
|
||||||
|
await SetupListByOrganizationRequestAsync(PermissionType.RunAsUserWithPermission);
|
||||||
|
var expectedAccess = await SetupServiceAccountSecretAccessAsync(serviceAccountIds, orgId);
|
||||||
|
|
||||||
|
var serviceAccountWithoutAccess = await _serviceAccountRepository.CreateAsync(new ServiceAccount
|
||||||
|
{
|
||||||
|
OrganizationId = orgId,
|
||||||
|
Name = _mockEncryptedString
|
||||||
|
});
|
||||||
|
|
||||||
|
var response =
|
||||||
|
await _client.GetAsync(
|
||||||
|
$"/organizations/{orgId}/service-accounts?includeAccessToSecrets={includeAccessToSecrets}");
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
var result = await response.Content
|
||||||
|
.ReadFromJsonAsync<ListResponseModel<ServiceAccountSecretsDetailsResponseModel>>();
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.NotEmpty(result.Data);
|
||||||
|
Assert.Equal(serviceAccountIds.Count, result.Data.Count());
|
||||||
|
Assert.DoesNotContain(result.Data, x => x.Id == serviceAccountWithoutAccess.Id);
|
||||||
|
|
||||||
|
if (includeAccessToSecrets)
|
||||||
|
{
|
||||||
|
foreach (var item in expectedAccess)
|
||||||
|
{
|
||||||
|
var serviceAccountResult = result.Data.FirstOrDefault(x => x.Id == item.Key);
|
||||||
|
Assert.NotNull(serviceAccountResult);
|
||||||
|
Assert.Equal(item.Value, serviceAccountResult.AccessToSecrets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Contains(result.Data, x => x.AccessToSecrets == 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -824,10 +867,10 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
|
|||||||
await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> { policy });
|
await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> { policy });
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<Guid>> SetupGetServiceAccountsByOrganizationAsync(Organization org)
|
private async Task<List<Guid>> CreateServiceAccountsInOrganizationAsync(Organization org)
|
||||||
{
|
{
|
||||||
var serviceAccountIds = new List<Guid>();
|
var serviceAccountIds = new List<Guid>();
|
||||||
for (var i = 0; i < 3; i++)
|
for (var i = 0; i < 4; i++)
|
||||||
{
|
{
|
||||||
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
|
var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount
|
||||||
{
|
{
|
||||||
@ -870,4 +913,109 @@ public class ServiceAccountsControllerTests : IClassFixture<ApiApplicationFactor
|
|||||||
|
|
||||||
return initialServiceAccount;
|
return initialServiceAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<(Guid OrganizationId, List<Guid> ServiceAccountIds)> SetupListByOrganizationRequestAsync(PermissionType permissionType)
|
||||||
|
{
|
||||||
|
var (org, _) = await _organizationHelper.Initialize(true, true, true);
|
||||||
|
await _loginHelper.LoginAsync(_email);
|
||||||
|
|
||||||
|
var serviceAccountIds = await CreateServiceAccountsInOrganizationAsync(org);
|
||||||
|
|
||||||
|
if (permissionType == PermissionType.RunAsAdmin)
|
||||||
|
{
|
||||||
|
return (org.Id, serviceAccountIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true);
|
||||||
|
await _loginHelper.LoginAsync(email);
|
||||||
|
|
||||||
|
var accessPolicies = serviceAccountIds.Select(
|
||||||
|
id => new UserServiceAccountAccessPolicy
|
||||||
|
{
|
||||||
|
OrganizationUserId = orgUser.Id,
|
||||||
|
GrantedServiceAccountId = id,
|
||||||
|
Read = true,
|
||||||
|
Write = false,
|
||||||
|
}).Cast<BaseAccessPolicy>().ToList();
|
||||||
|
|
||||||
|
await _accessPolicyRepository.CreateManyAsync(accessPolicies);
|
||||||
|
|
||||||
|
return (org.Id, serviceAccountIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Dictionary<Guid, int>> SetupServiceAccountSecretAccessAsync(List<Guid> serviceAccountIds,
|
||||||
|
Guid organizationId)
|
||||||
|
{
|
||||||
|
var project =
|
||||||
|
await _projectRepository.CreateAsync(new Project
|
||||||
|
{
|
||||||
|
Name = _mockEncryptedString,
|
||||||
|
OrganizationId = organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
var secret = await _secretRepository.CreateAsync(new Secret
|
||||||
|
{
|
||||||
|
Key = _mockEncryptedString,
|
||||||
|
Value = _mockEncryptedString,
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
Projects = [project]
|
||||||
|
});
|
||||||
|
|
||||||
|
var secretNoProject = await _secretRepository.CreateAsync(new Secret
|
||||||
|
{
|
||||||
|
Key = _mockEncryptedString,
|
||||||
|
Value = _mockEncryptedString,
|
||||||
|
OrganizationId = organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
var serviceAccountWithProjectAccess = serviceAccountIds[0];
|
||||||
|
var serviceAccountWithProjectAndSecretAccess = serviceAccountIds[1];
|
||||||
|
var serviceAccountWithSecretAccess = serviceAccountIds[2];
|
||||||
|
var serviceAccountWithNoAccess = serviceAccountIds[3];
|
||||||
|
await _accessPolicyRepository.CreateManyAsync([
|
||||||
|
new ServiceAccountProjectAccessPolicy
|
||||||
|
{
|
||||||
|
ServiceAccountId = serviceAccountWithProjectAccess,
|
||||||
|
GrantedProjectId = project.Id,
|
||||||
|
Read = true,
|
||||||
|
Write = true
|
||||||
|
},
|
||||||
|
new ServiceAccountProjectAccessPolicy
|
||||||
|
{
|
||||||
|
ServiceAccountId = serviceAccountWithProjectAndSecretAccess,
|
||||||
|
GrantedProjectId = project.Id,
|
||||||
|
Read = true,
|
||||||
|
Write = true
|
||||||
|
},
|
||||||
|
new ServiceAccountSecretAccessPolicy
|
||||||
|
{
|
||||||
|
ServiceAccountId = serviceAccountWithProjectAndSecretAccess,
|
||||||
|
GrantedSecretId = secret.Id,
|
||||||
|
Read = true,
|
||||||
|
Write = true
|
||||||
|
},
|
||||||
|
new ServiceAccountSecretAccessPolicy
|
||||||
|
{
|
||||||
|
ServiceAccountId = serviceAccountWithProjectAndSecretAccess,
|
||||||
|
GrantedSecretId = secretNoProject.Id,
|
||||||
|
Read = true,
|
||||||
|
Write = true
|
||||||
|
},
|
||||||
|
new ServiceAccountSecretAccessPolicy
|
||||||
|
{
|
||||||
|
ServiceAccountId = serviceAccountWithSecretAccess,
|
||||||
|
GrantedSecretId = secretNoProject.Id,
|
||||||
|
Read = true,
|
||||||
|
Write = true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
return new Dictionary<Guid, int>
|
||||||
|
{
|
||||||
|
{ serviceAccountWithProjectAccess, 1 },
|
||||||
|
{ serviceAccountWithProjectAndSecretAccess, 2 },
|
||||||
|
{ serviceAccountWithSecretAccess, 1 },
|
||||||
|
{ serviceAccountWithNoAccess, 0 }
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user