open-vault/vault/counters.go

240 lines
7.2 KiB
Go

package vault
import (
"context"
"fmt"
"sort"
"sync/atomic"
"time"
"github.com/hashicorp/vault/sdk/logical"
)
const (
requestCounterDatePathFormat = "2006/01"
// This storage path stores both the request counters in this file, and the activity log.
countersSubPath = "counters/"
requestCountersPath = "sys/counters/requests/"
)
type counters struct {
// requests counts requests seen by Vault this month; does not include requests
// excluded by design, e.g. health checks and UI asset requests.
requests *uint64
// activePath is set at startup to the path we primed the requests counter from,
// or empty string if there wasn't a relevant path - either because this is the first
// time Vault starts with the feature enabled, or because Vault hadn't written
// out the request counter this month yet.
// Whenever we write out the counters, we update activePath if it's no longer
// accurate. This coincides with a reset of the counters.
// There's no lock because the only reader/writer of activePath is the goroutine
// doing background syncs.
activePath string
// syncInterval determines how often the counters get written to storage (on primary)
// or synced to primary.
syncInterval time.Duration
}
// RequestCounter stores the state of request counters for a single unspecified period.
type RequestCounter struct {
// Total is the number of requests seen during a given period.
Total *uint64 `json:"total"`
}
// DatedRequestCounter holds request counters from a single period of time.
type DatedRequestCounter struct {
// StartTime is when the period starts.
StartTime time.Time `json:"start_time"`
// RequestCounter counts requests.
RequestCounter
}
// loadAllRequestCounters returns all request counters found in storage,
// ordered by time (oldest first.)
func (c *Core) loadAllRequestCounters(ctx context.Context, now time.Time) ([]DatedRequestCounter, error) {
view := NewBarrierView(c.barrier, requestCountersPath)
datepaths, err := view.List(ctx, "")
if err != nil {
return nil, fmt.Errorf("failed to read request counters: %w", err)
}
var all []DatedRequestCounter
sort.Strings(datepaths)
for _, datepath := range datepaths {
datesubpaths, err := view.List(ctx, datepath)
if err != nil {
return nil, fmt.Errorf("failed to read request counters: %w", err)
}
sort.Strings(datesubpaths)
for _, datesubpath := range datesubpaths {
fullpath := datepath + datesubpath
counter, err := c.loadRequestCounters(ctx, fullpath)
if err != nil {
return nil, err
}
t, err := time.Parse(requestCounterDatePathFormat, fullpath)
if err != nil {
return nil, err
}
all = append(all, DatedRequestCounter{StartTime: t, RequestCounter: *counter})
}
}
start, _ := time.Parse(requestCounterDatePathFormat, now.Format(requestCounterDatePathFormat))
idx := sort.Search(len(all), func(i int) bool {
return !all[i].StartTime.Before(start)
})
cur := atomic.LoadUint64(c.counters.requests)
if idx < len(all) {
all[idx].RequestCounter.Total = &cur
} else {
all = append(all, DatedRequestCounter{StartTime: start, RequestCounter: RequestCounter{Total: &cur}})
}
return all, nil
}
// loadCurrentRequestCounters reads the current RequestCounter out of storage.
// The in-memory current request counter is populated with the value read, if any.
// now should be the current time; it is a parameter to facilitate testing.
func (c *Core) loadCurrentRequestCounters(ctx context.Context, now time.Time) error {
datepath := now.Format(requestCounterDatePathFormat)
counter, err := c.loadRequestCounters(ctx, datepath)
if err != nil {
return err
}
if counter != nil {
c.counters.activePath = datepath
atomic.StoreUint64(c.counters.requests, *counter.Total)
}
return nil
}
// loadRequestCounters reads a RequestCounter out of storage at location datepath.
// If nothing is found at that path, that isn't an error: a reference to a zero
// RequestCounter is returned.
func (c *Core) loadRequestCounters(ctx context.Context, datepath string) (*RequestCounter, error) {
view := NewBarrierView(c.barrier, requestCountersPath)
out, err := view.Get(ctx, datepath)
if err != nil {
return nil, fmt.Errorf("failed to read request counters: %w", err)
}
if out == nil {
return nil, nil
}
newCounters := &RequestCounter{}
err = out.DecodeJSON(newCounters)
if err != nil {
return nil, err
}
return newCounters, nil
}
// saveCurrentRequestCounters writes the current RequestCounter to storage.
// The in-memory current request counter is reset to zero after writing if
// we've entered a new month.
// now should be the current time; it is a parameter to facilitate testing.
func (c *Core) saveCurrentRequestCounters(ctx context.Context, now time.Time) error {
view := NewBarrierView(c.barrier, requestCountersPath)
requests := atomic.LoadUint64(c.counters.requests)
curDatePath := now.Format(requestCounterDatePathFormat)
// If activePath is empty string, we were started with nothing in storage
// for the current month, so we should not reset the in-mem counter.
// But if activePath is nonempty and not curDatePath, we should reset.
shouldReset, writeDatePath := false, curDatePath
if c.counters.activePath != "" && c.counters.activePath != curDatePath {
shouldReset, writeDatePath = true, c.counters.activePath
}
localCounters := &RequestCounter{
Total: &requests,
}
entry, err := logical.StorageEntryJSON(writeDatePath, localCounters)
if err != nil {
return fmt.Errorf("failed to create request counters entry: %w", err)
}
if err := view.Put(ctx, entry); err != nil {
return fmt.Errorf("failed to save request counters: %w", err)
}
if shouldReset {
atomic.StoreUint64(c.counters.requests, 0)
}
if c.counters.activePath != curDatePath {
c.counters.activePath = curDatePath
}
return nil
}
// ActiveTokens contains the number of active tokens.
type ActiveTokens struct {
// ServiceTokens contains information about the number of active service
// tokens.
ServiceTokens TokenCounter `json:"service_tokens"`
}
// TokenCounter counts the number of tokens
type TokenCounter struct {
// Total is the total number of tokens
Total int `json:"total"`
}
// countActiveTokens returns the number of active tokens
func (c *Core) countActiveTokens(ctx context.Context) (*ActiveTokens, error) {
// Get all of the namespaces
ns := c.collectNamespaces()
// Count the tokens under each namespace
total := 0
for i := 0; i < len(ns); i++ {
ids, err := c.tokenStore.idView(ns[i]).List(ctx, "")
if err != nil {
return nil, err
}
total += len(ids)
}
return &ActiveTokens{
ServiceTokens: TokenCounter{
Total: total,
},
}, nil
}
// ActiveEntities contains the number of active entities.
type ActiveEntities struct {
// Entities contains information about the number of active entities.
Entities EntityCounter `json:"entities"`
}
// EntityCounter counts the number of entities
type EntityCounter struct {
// Total is the total number of entities
Total int `json:"total"`
}
// countActiveEntities returns the number of active entities
func (c *Core) countActiveEntities(ctx context.Context) (*ActiveEntities, error) {
count, err := c.identityStore.countEntities()
if err != nil {
return nil, err
}
return &ActiveEntities{
Entities: EntityCounter{
Total: count,
},
}, nil
}