Merge remote-tracking branch 'remotes/from/ce/main'

This commit is contained in:
hc-github-team-secure-vault-core 2026-04-29 14:41:56 +00:00
commit 132ad9c2e0
2 changed files with 275 additions and 116 deletions

View File

@ -151,9 +151,8 @@ func (b *SystemBackend) buildMonthBillingData(ctx context.Context, month time.Ti
// Build the usage metrics
usageMetrics := []map[string]interface{}{}
kvDetails := []map[string]interface{}{}
if combinedKvCounts > 0 {
kvDetails = append(kvDetails, map[string]interface{}{"type": "kv", "count": combinedKvCounts})
kvDetails := []map[string]interface{}{
{"type": "kv", "count": combinedKvCounts},
}
usageMetrics = append(usageMetrics, map[string]interface{}{
"metric_name": "static_secrets",
@ -181,15 +180,10 @@ func (b *SystemBackend) buildMonthBillingData(ctx context.Context, month time.Ti
},
})
dataProtectionDetails := []map[string]interface{}{}
if transitCounts > 0 {
dataProtectionDetails = append(dataProtectionDetails, map[string]interface{}{"type": "transit", "count": transitCounts})
}
if transformCounts > 0 {
dataProtectionDetails = append(dataProtectionDetails, map[string]interface{}{"type": "transform", "count": transformCounts})
}
if gcpKmsCounts > 0 {
dataProtectionDetails = append(dataProtectionDetails, map[string]interface{}{"type": "gcpkms", "count": gcpKmsCounts})
dataProtectionDetails := []map[string]interface{}{
{"type": "transit", "count": transitCounts},
{"type": "transform", "count": transformCounts},
{"type": "gcpkms", "count": gcpKmsCounts},
}
usageMetrics = append(usageMetrics, map[string]interface{}{
@ -206,12 +200,9 @@ func (b *SystemBackend) buildMonthBillingData(ctx context.Context, month time.Ti
}
usageMetrics = append(usageMetrics, pkiMetric)
managedKeysDetails := []map[string]interface{}{}
if combinedManagedKeyCounts.TotpKeys > 0 {
managedKeysDetails = append(managedKeysDetails, map[string]interface{}{"type": "totp", "count": combinedManagedKeyCounts.TotpKeys})
}
if combinedManagedKeyCounts.KmseKeys > 0 {
managedKeysDetails = append(managedKeysDetails, map[string]interface{}{"type": "kmse", "count": combinedManagedKeyCounts.KmseKeys})
managedKeysDetails := []map[string]interface{}{
{"type": "totp", "count": combinedManagedKeyCounts.TotpKeys},
{"type": "kmse", "count": combinedManagedKeyCounts.KmseKeys},
}
usageMetrics = append(usageMetrics, map[string]interface{}{
"metric_name": "managed_keys",
@ -281,63 +272,54 @@ func (c *Core) computeUpdatedAt(ctx context.Context, month, currentMonth time.Ti
// buildDynamicRolesMetric creates the dynamic_roles metric from role counts.
func buildDynamicRolesMetric(counts *RoleCounts) map[string]interface{} {
total := 0
awsCount := 0
azureCount := 0
databaseCount := 0
gcpCount := 0
ldapCount := 0
openldapCount := 0
alicloudCount := 0
rabbitmqCount := 0
consulCount := 0
nomadCount := 0
kubernetesCount := 0
mongodbatlasCount := 0
terraformCount := 0
if counts != nil {
total = counts.AWSDynamicRoles +
counts.AzureDynamicRoles +
counts.DatabaseDynamicRoles +
counts.GCPRolesets +
counts.LDAPDynamicRoles +
counts.OpenLDAPDynamicRoles +
counts.AlicloudDynamicRoles +
counts.RabbitMQDynamicRoles +
counts.ConsulDynamicRoles +
counts.NomadDynamicRoles +
counts.KubernetesDynamicRoles +
counts.MongoDBAtlasDynamicRoles +
counts.TerraformCloudDynamicRoles
awsCount = counts.AWSDynamicRoles
azureCount = counts.AzureDynamicRoles
databaseCount = counts.DatabaseDynamicRoles
gcpCount = counts.GCPRolesets
ldapCount = counts.LDAPDynamicRoles
openldapCount = counts.OpenLDAPDynamicRoles
alicloudCount = counts.AlicloudDynamicRoles
rabbitmqCount = counts.RabbitMQDynamicRoles
consulCount = counts.ConsulDynamicRoles
nomadCount = counts.NomadDynamicRoles
kubernetesCount = counts.KubernetesDynamicRoles
mongodbatlasCount = counts.MongoDBAtlasDynamicRoles
terraformCount = counts.TerraformCloudDynamicRoles
total = awsCount + azureCount + databaseCount + gcpCount + ldapCount +
openldapCount + alicloudCount + rabbitmqCount + consulCount +
nomadCount + kubernetesCount + mongodbatlasCount + terraformCount
}
details := []map[string]interface{}{}
if counts != nil {
if counts.AWSDynamicRoles > 0 {
details = append(details, map[string]interface{}{"type": "aws_dynamic", "count": counts.AWSDynamicRoles})
}
if counts.AzureDynamicRoles > 0 {
details = append(details, map[string]interface{}{"type": "azure_dynamic", "count": counts.AzureDynamicRoles})
}
if counts.DatabaseDynamicRoles > 0 {
details = append(details, map[string]interface{}{"type": "database_dynamic", "count": counts.DatabaseDynamicRoles})
}
if counts.GCPRolesets > 0 {
details = append(details, map[string]interface{}{"type": "gcp_dynamic", "count": counts.GCPRolesets})
}
if counts.LDAPDynamicRoles > 0 {
details = append(details, map[string]interface{}{"type": "ldap_dynamic", "count": counts.LDAPDynamicRoles})
}
if counts.OpenLDAPDynamicRoles > 0 {
details = append(details, map[string]interface{}{"type": "openldap_dynamic", "count": counts.OpenLDAPDynamicRoles})
}
if counts.AlicloudDynamicRoles > 0 {
details = append(details, map[string]interface{}{"type": "alicloud_dynamic", "count": counts.AlicloudDynamicRoles})
}
if counts.RabbitMQDynamicRoles > 0 {
details = append(details, map[string]interface{}{"type": "rabbitmq_dynamic", "count": counts.RabbitMQDynamicRoles})
}
if counts.ConsulDynamicRoles > 0 {
details = append(details, map[string]interface{}{"type": "consul_dynamic", "count": counts.ConsulDynamicRoles})
}
if counts.NomadDynamicRoles > 0 {
details = append(details, map[string]interface{}{"type": "nomad_dynamic", "count": counts.NomadDynamicRoles})
}
if counts.KubernetesDynamicRoles > 0 {
details = append(details, map[string]interface{}{"type": "kubernetes_dynamic", "count": counts.KubernetesDynamicRoles})
}
if counts.MongoDBAtlasDynamicRoles > 0 {
details = append(details, map[string]interface{}{"type": "mongodbatlas_dynamic", "count": counts.MongoDBAtlasDynamicRoles})
}
if counts.TerraformCloudDynamicRoles > 0 {
details = append(details, map[string]interface{}{"type": "terraform_dynamic", "count": counts.TerraformCloudDynamicRoles})
}
details := []map[string]interface{}{
{"type": "aws_dynamic", "count": awsCount},
{"type": "azure_dynamic", "count": azureCount},
{"type": "database_dynamic", "count": databaseCount},
{"type": "gcp_dynamic", "count": gcpCount},
{"type": "ldap_dynamic", "count": ldapCount},
{"type": "openldap_dynamic", "count": openldapCount},
{"type": "alicloud_dynamic", "count": alicloudCount},
{"type": "rabbitmq_dynamic", "count": rabbitmqCount},
{"type": "consul_dynamic", "count": consulCount},
{"type": "nomad_dynamic", "count": nomadCount},
{"type": "kubernetes_dynamic", "count": kubernetesCount},
{"type": "mongodbatlas_dynamic", "count": mongodbatlasCount},
{"type": "terraform_dynamic", "count": terraformCount},
}
return map[string]interface{}{
@ -352,39 +334,35 @@ func buildDynamicRolesMetric(counts *RoleCounts) map[string]interface{} {
// buildAutoRotatedRolesMetric creates the auto_rotated_roles metric from role counts.
func buildAutoRotatedRolesMetric(counts *RoleCounts) map[string]interface{} {
total := 0
awsCount := 0
azureCount := 0
databaseCount := 0
gcpStaticCount := 0
gcpImpersonatedCount := 0
ldapCount := 0
openldapCount := 0
if counts != nil {
total = counts.AWSStaticRoles +
counts.AzureStaticRoles +
counts.DatabaseStaticRoles +
counts.GCPStaticAccounts +
counts.GCPImpersonatedAccounts +
counts.LDAPStaticRoles +
counts.OpenLDAPStaticRoles
awsCount = counts.AWSStaticRoles
azureCount = counts.AzureStaticRoles
databaseCount = counts.DatabaseStaticRoles
gcpStaticCount = counts.GCPStaticAccounts
gcpImpersonatedCount = counts.GCPImpersonatedAccounts
ldapCount = counts.LDAPStaticRoles
openldapCount = counts.OpenLDAPStaticRoles
total = awsCount + azureCount + databaseCount + gcpStaticCount +
gcpImpersonatedCount + ldapCount + openldapCount
}
details := []map[string]interface{}{}
if counts != nil {
if counts.AWSStaticRoles > 0 {
details = append(details, map[string]interface{}{"type": "aws_static", "count": counts.AWSStaticRoles})
}
if counts.AzureStaticRoles > 0 {
details = append(details, map[string]interface{}{"type": "azure_static", "count": counts.AzureStaticRoles})
}
if counts.DatabaseStaticRoles > 0 {
details = append(details, map[string]interface{}{"type": "database_static", "count": counts.DatabaseStaticRoles})
}
if counts.GCPStaticAccounts > 0 {
details = append(details, map[string]interface{}{"type": "gcp_static", "count": counts.GCPStaticAccounts})
}
if counts.GCPImpersonatedAccounts > 0 {
details = append(details, map[string]interface{}{"type": "gcp_impersonated", "count": counts.GCPImpersonatedAccounts})
}
if counts.LDAPStaticRoles > 0 {
details = append(details, map[string]interface{}{"type": "ldap_static", "count": counts.LDAPStaticRoles})
}
if counts.OpenLDAPStaticRoles > 0 {
details = append(details, map[string]interface{}{"type": "openldap_static", "count": counts.OpenLDAPStaticRoles})
}
details := []map[string]interface{}{
{"type": "aws_static", "count": awsCount},
{"type": "azure_static", "count": azureCount},
{"type": "database_static", "count": databaseCount},
{"type": "gcp_static", "count": gcpStaticCount},
{"type": "gcp_impersonated", "count": gcpImpersonatedCount},
{"type": "ldap_static", "count": ldapCount},
{"type": "openldap_static", "count": openldapCount},
}
return map[string]interface{}{
@ -415,16 +393,11 @@ func (b *SystemBackend) buildPkiBillingMetric(ctx context.Context, month time.Ti
func (b *SystemBackend) buildIdTokenUnitsBillingMetric(ctx context.Context, month time.Time) (map[string]interface{}, error) {
var totalTokens float64
idTokenDetails := []map[string]interface{}{}
oidcTokenCount, err := b.Core.GetStoredOidcDurationAdjustedCount(ctx, month)
if err != nil {
return nil, fmt.Errorf("error retrieving OIDC duration-adjusted token count for month: %w", err)
}
if oidcTokenCount > 0 {
idTokenDetails = append(idTokenDetails, map[string]interface{}{"type": "oidc", "count": oidcTokenCount})
}
totalTokens += oidcTokenCount
spiffeJwtUnits, err := b.Core.GetStoredSpiffeJwtTokenUnits(ctx, month)
@ -432,12 +405,13 @@ func (b *SystemBackend) buildIdTokenUnitsBillingMetric(ctx context.Context, mont
return nil, fmt.Errorf("error retrieving JWT Spiffe duration-adjusted token count for month: %w", err)
}
if spiffeJwtUnits > 0 {
idTokenDetails = append(idTokenDetails, map[string]interface{}{"type": "spiffe", "count": spiffeJwtUnits})
}
totalTokens += spiffeJwtUnits
idTokenDetails := []map[string]interface{}{
{"type": "oidc", "count": oidcTokenCount},
{"type": "spiffe", "count": spiffeJwtUnits},
}
return map[string]interface{}{
"metric_name": "id_token_units",
"metric_data": map[string]interface{}{

View File

@ -618,14 +618,33 @@ func TestSystemBackend_BillingOverview_EmptyMetrics(t *testing.T) {
// Verify each metric has appropriate zero value
switch metricName {
case "static_secrets", "dynamic_roles", "auto_rotated_roles":
case "static_secrets":
total, ok := metricData["total"].(int)
require.True(t, ok, "%s total should be int", metricName)
require.Equal(t, 0, total, "%s total should be 0", metricName)
details, ok := metricData["metric_details"].([]map[string]interface{})
require.True(t, ok, "%s metric_details should be array", metricName)
require.Empty(t, details, "%s metric_details should be empty when total is 0", metricName)
require.NotEmpty(t, details, "%s metric_details should always be present", metricName)
// Verify kv type is present with zero count
require.Len(t, details, 1)
require.Equal(t, "kv", details[0]["type"])
require.Equal(t, 0, details[0]["count"])
case "dynamic_roles", "auto_rotated_roles":
total, ok := metricData["total"].(int)
require.True(t, ok, "%s total should be int", metricName)
require.Equal(t, 0, total, "%s total should be 0", metricName)
details, ok := metricData["metric_details"].([]map[string]interface{})
require.True(t, ok, "%s metric_details should be array", metricName)
require.NotEmpty(t, details, "%s metric_details should always be present", metricName)
// Verify all role types are present with zero counts
for _, detail := range details {
require.Contains(t, detail, "type")
require.Contains(t, detail, "count")
require.Equal(t, 0, detail["count"])
}
case "kmip":
used, ok := metricData["used_in_month"].(bool)
@ -644,7 +663,18 @@ func TestSystemBackend_BillingOverview_EmptyMetrics(t *testing.T) {
details, ok := metricData["metric_details"].([]map[string]interface{})
require.True(t, ok, "data_protection_calls metric_details should be array")
require.Empty(t, details, "data_protection_calls metric_details should be empty when total is 0")
require.NotEmpty(t, details, "data_protection_calls metric_details should always be present")
// Verify all data protection types are present with zero counts
require.Len(t, details, 3)
expectedTypes := map[string]bool{"transit": false, "transform": false, "gcpkms": false}
for _, detail := range details {
detailType := detail["type"].(string)
expectedTypes[detailType] = true
require.Equal(t, uint64(0), detail["count"])
}
for typeName, found := range expectedTypes {
require.True(t, found, "type %s should be present", typeName)
}
case "pki_units":
total, ok := metricData["total"].(float64)
@ -653,11 +683,22 @@ func TestSystemBackend_BillingOverview_EmptyMetrics(t *testing.T) {
case "managed_keys":
total, ok := metricData["total"].(int)
require.True(t, ok, "managed_keys total should be float64")
require.True(t, ok, "managed_keys total should be int")
require.Equal(t, int(0), total, "managed keys total should be 0")
details, ok := metricData["metric_details"].([]map[string]interface{})
require.True(t, ok, "%s metric_details should be array", metricName)
require.Empty(t, details, "%s metric_details should be empty when total is 0", metricName)
require.NotEmpty(t, details, "%s metric_details should always be present", metricName)
// Verify both managed key types are present with zero counts
require.Len(t, details, 2)
expectedTypes := map[string]bool{"totp": false, "kmse": false}
for _, detail := range details {
detailType := detail["type"].(string)
expectedTypes[detailType] = true
require.Equal(t, 0, detail["count"])
}
for typeName, found := range expectedTypes {
require.True(t, found, "type %s should be present", typeName)
}
case "ssh_units":
total, ok := metricData["total"].(float64)
@ -668,6 +709,21 @@ func TestSystemBackend_BillingOverview_EmptyMetrics(t *testing.T) {
total, ok := metricData["total"].(float64)
require.True(t, ok, "id_token_units total should be float64")
require.Equal(t, float64(0), total, "id_token_units total should be 0")
details, ok := metricData["metric_details"].([]map[string]interface{})
require.True(t, ok, "id_token_units metric_details should be array")
require.NotEmpty(t, details, "id_token_units metric_details should always be present")
// Verify both token types are present with zero counts
require.Len(t, details, 2)
expectedTypes := map[string]bool{"oidc": false, "spiffe": false}
for _, detail := range details {
detailType := detail["type"].(string)
expectedTypes[detailType] = true
require.Equal(t, float64(0), detail["count"])
}
for typeName, found := range expectedTypes {
require.True(t, found, "type %s should be present", typeName)
}
}
}
@ -941,6 +997,135 @@ func TestSystemBackend_BillingOverview_UpdatedAtTimestamp_NoStoredTimestamp(t *t
"previous month updated_at should be zero time when no stored timestamp exists")
}
// TestSystemBackend_BillingOverview_AllMetricTypesPresent verifies that all metric types
// are always present in the response, even when their counts are zero. This test specifically
// validates that metric_details arrays contain all expected types for each metric category.
func TestSystemBackend_BillingOverview_AllMetricTypesPresent(t *testing.T) {
_, b, _ := testCoreSystemBackend(t)
ctx := namespace.RootContext(nil)
// Make a request without creating any billable resources
req := logical.TestRequest(t, logical.ReadOperation, "billing/overview")
resp, err := b.HandleRequest(ctx, req)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Data)
// Verify the response structure exists
months, ok := resp.Data["months"].([]interface{})
require.True(t, ok)
require.Len(t, months, billing.BillingRetentionMonths)
// Check current month has all metrics
currentMonth, ok := months[0].(map[string]interface{})
require.True(t, ok)
require.Contains(t, currentMonth, "usage_metrics")
usageMetrics, ok := currentMonth["usage_metrics"].([]map[string]interface{})
require.True(t, ok)
require.NotNil(t, usageMetrics)
require.NotEmpty(t, usageMetrics, "usage_metrics should contain all metrics even with zero values")
// Build a map of metrics for easy lookup
metricsMap := make(map[string]map[string]interface{})
for _, metric := range usageMetrics {
metricName, ok := metric["metric_name"].(string)
require.True(t, ok, "metric_name should be a string")
metricsMap[metricName] = metric
}
// Verify static_secrets has kv type
staticSecretsMetric, exists := metricsMap["static_secrets"]
require.True(t, exists, "static_secrets metric should be present")
staticSecretsData := staticSecretsMetric["metric_data"].(map[string]interface{})
staticSecretsDetails := staticSecretsData["metric_details"].([]map[string]interface{})
require.Len(t, staticSecretsDetails, 1, "static_secrets should have 1 type")
require.Equal(t, "kv", staticSecretsDetails[0]["type"])
require.Equal(t, 0, staticSecretsDetails[0]["count"])
// Verify dynamic_roles has all 13 types
dynamicRolesMetric, exists := metricsMap["dynamic_roles"]
require.True(t, exists, "dynamic_roles metric should be present")
dynamicRolesData := dynamicRolesMetric["metric_data"].(map[string]interface{})
dynamicRolesDetails := dynamicRolesData["metric_details"].([]map[string]interface{})
require.Len(t, dynamicRolesDetails, 13, "dynamic_roles should have 13 types")
expectedDynamicTypes := []string{
"aws_dynamic", "azure_dynamic", "database_dynamic", "gcp_dynamic",
"ldap_dynamic", "openldap_dynamic", "alicloud_dynamic", "rabbitmq_dynamic",
"consul_dynamic", "nomad_dynamic", "kubernetes_dynamic", "mongodbatlas_dynamic",
"terraform_dynamic",
}
for i, expectedType := range expectedDynamicTypes {
require.Equal(t, expectedType, dynamicRolesDetails[i]["type"], "dynamic role type at index %d should be %s", i, expectedType)
require.Equal(t, 0, dynamicRolesDetails[i]["count"], "dynamic role count at index %d should be 0", i)
}
// Verify auto_rotated_roles has all 7 types
autoRotatedMetric, exists := metricsMap["auto_rotated_roles"]
require.True(t, exists, "auto_rotated_roles metric should be present")
autoRotatedData := autoRotatedMetric["metric_data"].(map[string]interface{})
autoRotatedDetails := autoRotatedData["metric_details"].([]map[string]interface{})
require.Len(t, autoRotatedDetails, 7, "auto_rotated_roles should have 7 types")
expectedAutoRotatedTypes := []string{
"aws_static", "azure_static", "database_static", "gcp_static",
"gcp_impersonated", "ldap_static", "openldap_static",
}
for i, expectedType := range expectedAutoRotatedTypes {
require.Equal(t, expectedType, autoRotatedDetails[i]["type"], "auto-rotated role type at index %d should be %s", i, expectedType)
require.Equal(t, 0, autoRotatedDetails[i]["count"], "auto-rotated role count at index %d should be 0", i)
}
// Verify data_protection_calls has all 3 types
dataProtectionMetric, exists := metricsMap["data_protection_calls"]
require.True(t, exists, "data_protection_calls metric should be present")
dataProtectionData := dataProtectionMetric["metric_data"].(map[string]interface{})
dataProtectionDetails := dataProtectionData["metric_details"].([]map[string]interface{})
require.Len(t, dataProtectionDetails, 3, "data_protection_calls should have 3 types")
expectedDataProtectionTypes := []string{"transit", "transform", "gcpkms"}
for i, expectedType := range expectedDataProtectionTypes {
require.Equal(t, expectedType, dataProtectionDetails[i]["type"], "data protection type at index %d should be %s", i, expectedType)
require.Equal(t, uint64(0), dataProtectionDetails[i]["count"], "data protection count at index %d should be 0", i)
}
// Verify managed_keys has both types
managedKeysMetric, exists := metricsMap["managed_keys"]
require.True(t, exists, "managed_keys metric should be present")
managedKeysData := managedKeysMetric["metric_data"].(map[string]interface{})
managedKeysDetails := managedKeysData["metric_details"].([]map[string]interface{})
require.Len(t, managedKeysDetails, 2, "managed_keys should have 2 types")
expectedManagedKeyTypes := []string{"totp", "kmse"}
for i, expectedType := range expectedManagedKeyTypes {
require.Equal(t, expectedType, managedKeysDetails[i]["type"], "managed key type at index %d should be %s", i, expectedType)
require.Equal(t, 0, managedKeysDetails[i]["count"], "managed key count at index %d should be 0", i)
}
// Verify ssh_units has both types
sshMetric, exists := metricsMap["ssh_units"]
require.True(t, exists, "ssh_units metric should be present")
sshData := sshMetric["metric_data"].(map[string]interface{})
sshDetails := sshData["metric_details"].([]map[string]interface{})
require.Len(t, sshDetails, 2, "ssh_units should have 2 types")
require.Equal(t, "otp_units", sshDetails[0]["type"])
require.Equal(t, "certificate_units", sshDetails[1]["type"])
// Verify id_token_units has both types
idTokenMetric, exists := metricsMap["id_token_units"]
require.True(t, exists, "id_token_units metric should be present")
idTokenData := idTokenMetric["metric_data"].(map[string]interface{})
idTokenDetails := idTokenData["metric_details"].([]map[string]interface{})
require.Len(t, idTokenDetails, 2, "id_token_units should have 2 types")
expectedIdTokenTypes := []string{"oidc", "spiffe"}
for i, expectedType := range expectedIdTokenTypes {
require.Equal(t, expectedType, idTokenDetails[i]["type"], "id token type at index %d should be %s", i, expectedType)
require.Equal(t, float64(0), idTokenDetails[i]["count"], "id token count at index %d should be 0", i)
}
}
// TestSystemBackend_BillingOverview_PreviousMonth_WithError tests the behavior
// when retrieving the previous month's timestamp fails with an error.
// This ensures the endpoint gracefully handles storage errors by returning zero time.