mirror of
https://github.com/hashicorp/vault.git
synced 2026-06-09 08:55:13 -04:00
Merge remote-tracking branch 'remotes/from/ce/release/2.x.x' into release/2.x.x
This commit is contained in:
commit
2c4853d23b
9 changed files with 175 additions and 17 deletions
|
|
@ -143,7 +143,7 @@ const billingOverviewResponse = `{
|
|||
{
|
||||
"metric_name": "auto_rotated_roles",
|
||||
"metric_data": {
|
||||
"total": 10,
|
||||
"total": 15,
|
||||
"metric_details": [
|
||||
{
|
||||
"type": "aws_static",
|
||||
|
|
@ -152,6 +152,10 @@ const billingOverviewResponse = `{
|
|||
{
|
||||
"type": "azure_static",
|
||||
"count": 5
|
||||
},
|
||||
{
|
||||
"type": "os_local_account_static",
|
||||
"count": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -203,10 +207,10 @@ const billingOverviewResponse = `{
|
|||
"type": "totp",
|
||||
"count": 5
|
||||
},
|
||||
{
|
||||
"type": "kmse",
|
||||
"count": 5
|
||||
}
|
||||
{
|
||||
"type": "kmse",
|
||||
"count": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
|
|||
3
changelog/_14467.txt
Normal file
3
changelog/_14467.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
consumption-billing: Add billing tracking for OS Local Account static roles to support consumption-based billing metrics and high-water mark (HWM) tracking.
|
||||
```
|
||||
|
|
@ -65,6 +65,7 @@ const (
|
|||
// SecretEngineDatabase is the entry type for all databases, i.e. this is the combined
|
||||
// database type for every database.
|
||||
SecretEngineDatabase = "database"
|
||||
SecretEngineOS = "vault-plugin-secrets-os"
|
||||
)
|
||||
|
||||
// These DB consts match the type returned from database plugin's Type() method.
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ func TestHandleEndOfMonthMetrics(t *testing.T) {
|
|||
GCPRolesets: 3,
|
||||
DatabaseDynamicRoles: 5,
|
||||
DatabaseStaticRoles: 7,
|
||||
OSLocalAccountRoles: 9,
|
||||
}, localPathPrefix, month)
|
||||
core.storeMaxKvCountsLocked(context.Background(), 10, localPathPrefix, month)
|
||||
|
||||
|
|
@ -252,9 +253,10 @@ func TestDeleteExpiredBillingMetrics(t *testing.T) {
|
|||
for _, month := range []time.Time{monthToDelete, oldestRetainedMonth, currentMonth} {
|
||||
for _, pathPrefix := range []string{billing.ReplicatedPrefix, billing.LocalPrefix} {
|
||||
core.storeMaxRoleCountsLocked(context.Background(), &RoleCounts{
|
||||
AWSDynamicRoles: 5,
|
||||
AWSStaticRoles: 10,
|
||||
LDAPDynamicRoles: 3,
|
||||
AWSDynamicRoles: 5,
|
||||
AWSStaticRoles: 10,
|
||||
LDAPDynamicRoles: 3,
|
||||
OSLocalAccountRoles: 7,
|
||||
}, pathPrefix, month)
|
||||
core.storeMaxKvCountsLocked(context.Background(), 20, pathPrefix, month)
|
||||
core.storeTransitCallCountsLocked(context.Background(), 15, pathPrefix, month)
|
||||
|
|
@ -407,6 +409,7 @@ func TestConsumptionBillingMetricsWorkerWithCustomClock(t *testing.T) {
|
|||
KubernetesDynamicRoles: 5,
|
||||
MongoDBAtlasDynamicRoles: 7,
|
||||
TerraformCloudDynamicRoles: 10,
|
||||
OSLocalAccountRoles: 11,
|
||||
}
|
||||
|
||||
verifyMonthlyBillingMetrics := func(month time.Time, localPathPrefix string) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,14 @@
|
|||
|
||||
package vault
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
func (c *Core) ResetInMemoryTransitDataProtectionCallCounts() {
|
||||
c.consumptionBillingLock.RLock()
|
||||
cb := c.consumptionBilling
|
||||
|
|
@ -137,3 +145,106 @@ func (c *Core) SetInMemoryOidcCounts(tokenDuration float64) {
|
|||
cb.IdentityTokenUnits.OidcTokenDuration.Store(tokenDuration)
|
||||
}
|
||||
}
|
||||
|
||||
// NewMockOSBackendFactory creates a mock OS backend factory for testing.
|
||||
// The backend implements LIST operations for hosts and accounts to support
|
||||
// billing enumeration testing.
|
||||
func NewMockOSBackendFactory() logical.Factory {
|
||||
return func(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
|
||||
b := &framework.Backend{
|
||||
BackendType: logical.TypeLogical,
|
||||
Paths: []*framework.Path{
|
||||
{
|
||||
Pattern: "hosts/?$",
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ListOperation: &framework.PathOperation{
|
||||
Callback: func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
// List all hosts from storage
|
||||
hosts, err := req.Storage.List(ctx, "hosts/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return logical.ListResponse(hosts), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Pattern: "hosts/" + framework.GenericNameRegex("host") + "/accounts/?$",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"host": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Host name",
|
||||
},
|
||||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ListOperation: &framework.PathOperation{
|
||||
Callback: func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
// Get the host name from the path
|
||||
hostName := data.Get("host").(string)
|
||||
|
||||
// Read the host entry from storage
|
||||
entry, err := req.Storage.Get(ctx, "hosts/"+hostName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry == nil {
|
||||
return logical.ListResponse([]string{}), nil
|
||||
}
|
||||
|
||||
// Parse the JSON to extract account names
|
||||
var hostData map[string]interface{}
|
||||
if err := json.Unmarshal(entry.Value, &hostData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accounts := []string{}
|
||||
if accountsMap, ok := hostData["accounts"].(map[string]interface{}); ok {
|
||||
for accountName := range accountsMap {
|
||||
accounts = append(accounts, accountName)
|
||||
}
|
||||
}
|
||||
|
||||
return logical.ListResponse(accounts), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := b.Setup(ctx, conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
}
|
||||
|
||||
// CreateMockOSHost creates a mock OS host entry in storage with the specified accounts.
|
||||
// This is a helper function for tests that need to populate OS backend storage.
|
||||
func CreateMockOSHost(ctx context.Context, storage logical.Storage, hostName string, accountNames []string) error {
|
||||
// Build the accounts map structure
|
||||
accountsMap := make(map[string]interface{})
|
||||
for _, accountName := range accountNames {
|
||||
accountsMap[accountName] = map[string]string{"username": "testuser"}
|
||||
}
|
||||
|
||||
// Create the host entry structure and marshal to JSON
|
||||
hostMap := map[string]interface{}{"accounts": accountsMap}
|
||||
value, err := json.Marshal(hostMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a mock host entry with accounts
|
||||
hostEntry := &logical.StorageEntry{
|
||||
Key: "hosts/" + hostName,
|
||||
Value: value,
|
||||
}
|
||||
return storage.Put(ctx, hostEntry)
|
||||
}
|
||||
|
||||
// DeleteMockOSHost deletes a mock OS host entry from storage.
|
||||
// This is a helper function for tests that need to clean up OS backend storage.
|
||||
func DeleteMockOSHost(ctx context.Context, storage logical.Storage, hostName string) error {
|
||||
return storage.Delete(ctx, "hosts/"+hostName)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ func combineRoleCounts(a, b *RoleCounts) *RoleCounts {
|
|||
a.KubernetesDynamicRoles + b.KubernetesDynamicRoles,
|
||||
a.MongoDBAtlasDynamicRoles + b.MongoDBAtlasDynamicRoles,
|
||||
a.TerraformCloudDynamicRoles + b.TerraformCloudDynamicRoles,
|
||||
a.OSLocalAccountRoles + b.OSLocalAccountRoles,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -321,6 +322,7 @@ func (c *Core) updateMaxRoleCounts(ctx context.Context, currentRoleCounts *RoleC
|
|||
maxRoleCounts.KubernetesDynamicRoles = c.compareCounts(currentRoleCounts.KubernetesDynamicRoles, maxRoleCounts.KubernetesDynamicRoles, "Kubernetes Dynamic Roles")
|
||||
maxRoleCounts.MongoDBAtlasDynamicRoles = c.compareCounts(currentRoleCounts.MongoDBAtlasDynamicRoles, maxRoleCounts.MongoDBAtlasDynamicRoles, "MongoDB Atlas Dynamic Roles")
|
||||
maxRoleCounts.TerraformCloudDynamicRoles = c.compareCounts(currentRoleCounts.TerraformCloudDynamicRoles, maxRoleCounts.TerraformCloudDynamicRoles, "Terraform Cloud Dynamic Roles")
|
||||
maxRoleCounts.OSLocalAccountRoles = c.compareCounts(currentRoleCounts.OSLocalAccountRoles, maxRoleCounts.OSLocalAccountRoles, "OS Local Account Static Roles")
|
||||
|
||||
err = c.storeMaxRoleCountsLocked(ctx, maxRoleCounts, localPathPrefix, currentMonth)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -865,6 +865,7 @@ type RoleCounts struct {
|
|||
KubernetesDynamicRoles int `json:"kubernetes_dynamic_roles"`
|
||||
MongoDBAtlasDynamicRoles int `json:"mongodb_atlas_dynamic_roles"`
|
||||
TerraformCloudDynamicRoles int `json:"terraformcloud_dynamic_roles"`
|
||||
OSLocalAccountRoles int `json:"os_local_account_static_roles"`
|
||||
}
|
||||
|
||||
type ManagedKeyCounts struct {
|
||||
|
|
@ -890,18 +891,33 @@ func (c *Core) getRoleAndManagedKeyCountsInternal(includeLocal bool, includeRepl
|
|||
}
|
||||
|
||||
resp, err := c.router.Route(ctx, listRequest)
|
||||
if err != nil || resp == nil {
|
||||
if err != nil || resp == nil || resp.Data == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
rawKeys, ok := resp.Data["keys"]
|
||||
if !ok {
|
||||
if !ok || rawKeys == nil {
|
||||
return nil
|
||||
}
|
||||
keys, ok := rawKeys.([]string)
|
||||
if !ok {
|
||||
|
||||
// Type switch handles both the 'official' behavior and the 'generic' behavior
|
||||
switch kt := rawKeys.(type) {
|
||||
case []string:
|
||||
// Existing plugins likely hit this path
|
||||
return kt
|
||||
case []interface{}:
|
||||
// External/RPC plugins likely hit this path
|
||||
keys := make([]string, 0, len(kt))
|
||||
for _, k := range kt {
|
||||
if s, ok := k.(string); ok {
|
||||
keys = append(keys, s)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
default:
|
||||
// If it's something totally weird, we still fail safely
|
||||
return nil
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
c.mountsLock.RLock()
|
||||
|
|
@ -1009,6 +1025,21 @@ func (c *Core) getRoleAndManagedKeyCountsInternal(includeLocal bool, includeRepl
|
|||
case pluginconsts.SecretEngineKeymgmt:
|
||||
keyCountPerEntry := apiList(entry, "key")
|
||||
keyCounts.KmseKeys += len(keyCountPerEntry)
|
||||
|
||||
case pluginconsts.SecretEngineOS:
|
||||
// OS plugin stores all accounts within each host entry
|
||||
// List all hosts, then list accounts for each host
|
||||
hosts := apiList(entry, "hosts/")
|
||||
accountCount := 0
|
||||
for _, host := range hosts {
|
||||
if host == "" {
|
||||
continue
|
||||
}
|
||||
hostName := strings.TrimSuffix(host, "/")
|
||||
accounts := apiList(entry, "hosts/"+hostName+"/accounts/")
|
||||
accountCount += len(accounts)
|
||||
}
|
||||
roles.OSLocalAccountRoles += accountCount
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -436,6 +436,7 @@ func buildAutoRotatedRolesMetric(counts *RoleCounts) map[string]interface{} {
|
|||
gcpImpersonatedCount := 0
|
||||
ldapCount := 0
|
||||
openldapCount := 0
|
||||
osLocalAccountCount := 0
|
||||
|
||||
if counts != nil {
|
||||
awsCount = counts.AWSStaticRoles
|
||||
|
|
@ -445,9 +446,10 @@ func buildAutoRotatedRolesMetric(counts *RoleCounts) map[string]interface{} {
|
|||
gcpImpersonatedCount = counts.GCPImpersonatedAccounts
|
||||
ldapCount = counts.LDAPStaticRoles
|
||||
openldapCount = counts.OpenLDAPStaticRoles
|
||||
osLocalAccountCount = counts.OSLocalAccountRoles
|
||||
|
||||
total = awsCount + azureCount + databaseCount + gcpStaticCount +
|
||||
gcpImpersonatedCount + ldapCount + openldapCount
|
||||
gcpImpersonatedCount + ldapCount + openldapCount + osLocalAccountCount
|
||||
}
|
||||
|
||||
details := []map[string]interface{}{
|
||||
|
|
@ -458,6 +460,7 @@ func buildAutoRotatedRolesMetric(counts *RoleCounts) map[string]interface{} {
|
|||
{"type": "gcp_impersonated", "count": gcpImpersonatedCount},
|
||||
{"type": "ldap_static", "count": ldapCount},
|
||||
{"type": "openldap_static", "count": openldapCount},
|
||||
{"type": "os_local_account_static", "count": osLocalAccountCount},
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
|
|
|
|||
|
|
@ -1225,16 +1225,16 @@ func TestSystemBackend_BillingOverview_AllMetricTypesPresent(t *testing.T) {
|
|||
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
|
||||
// Verify auto_rotated_roles has all 8 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")
|
||||
require.Len(t, autoRotatedDetails, 8, "auto_rotated_roles should have 8 types")
|
||||
|
||||
expectedAutoRotatedTypes := []string{
|
||||
"aws_static", "azure_static", "database_static", "gcp_static",
|
||||
"gcp_impersonated", "ldap_static", "openldap_static",
|
||||
"gcp_impersonated", "ldap_static", "openldap_static", "os_local_account_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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue