vault/sdk/helper/clientcountutil/clientcountutil.go
miagilepner 6fd8cb6409
[VAULT-15398] Client count tests (#22635)
* fix bugs in client count data generation

* add new tests for client counts

* fix package name
2023-09-01 11:32:40 +02:00

406 lines
14 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Package clientcountutil provides a library to generate activity log data for
// testing.
package clientcountutil
import (
"context"
"errors"
"fmt"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/sdk/helper/clientcountutil/generation"
"google.golang.org/protobuf/encoding/protojson"
)
// ActivityLogDataGenerator holds an ActivityLogMockInput. Users can create the
// generator with NewActivityLogData(), add content to the generator using
// the fluent API methods, and generate and write the JSON representation of the
// input to the Vault API.
type ActivityLogDataGenerator struct {
data *generation.ActivityLogMockInput
addingToMonth *generation.Data
addingToSegment *generation.Segment
client *api.Client
}
// NewActivityLogData creates a new instance of an activity log data generator
// The type returned by this function cannot be called concurrently
func NewActivityLogData(client *api.Client) *ActivityLogDataGenerator {
return &ActivityLogDataGenerator{
client: client,
data: new(generation.ActivityLogMockInput),
}
}
// NewCurrentMonthData opens a new month of data for the current month. All
// clients will continue to be added to this month until a new month is created
// with NewPreviousMonthData.
func (d *ActivityLogDataGenerator) NewCurrentMonthData() *ActivityLogDataGenerator {
return d.newMonth(&generation.Data{Month: &generation.Data_CurrentMonth{CurrentMonth: true}})
}
// NewPreviousMonthData opens a new month of data, where the clients will be
// recorded as having been seen monthsAgo months ago. All clients will continue
// to be added to this month until a new month is created with
// NewPreviousMonthData or NewCurrentMonthData.
func (d *ActivityLogDataGenerator) NewPreviousMonthData(monthsAgo int) *ActivityLogDataGenerator {
return d.newMonth(&generation.Data{Month: &generation.Data_MonthsAgo{MonthsAgo: int32(monthsAgo)}})
}
func (d *ActivityLogDataGenerator) newMonth(newMonth *generation.Data) *ActivityLogDataGenerator {
d.data.Data = append(d.data.Data, newMonth)
d.addingToMonth = newMonth
d.addingToSegment = nil
return d
}
// MonthOption holds an option that can be set for the entire month
type MonthOption func(m *generation.Data)
// WithMaximumSegmentIndex sets the maximum segment index for the segments in
// the open month. Set this value in order to set how many indexes the data
// should be split across. This must include any empty or skipped indexes. For
// example, say that you would like all of your data split across indexes 0 and
// 3, with the following empty and skipped indexes:
//
// empty indexes: [2]
// skipped indexes: [1]
//
// To accomplish that, you will need to call WithMaximumSegmentIndex(3).
// This value will be ignored if you have called Segment() for the open month
// If not set, all data will be in 1 segment.
func WithMaximumSegmentIndex(n int) MonthOption {
return func(m *generation.Data) {
m.NumSegments = int32(n)
}
}
// WithEmptySegmentIndexes sets which segment indexes should be empty for the
// segments in the open month. If you use this option, you must either:
// 1. ensure that you've called Segment() for the open month
// 2. use WithMaximumSegmentIndex() to set the total number of segments
//
// If you haven't set either of those values then this option will be ignored,
// unless you included 0 as an empty segment index in which case only an empty
// segment will be created.
func WithEmptySegmentIndexes(i ...int) MonthOption {
return func(m *generation.Data) {
indexes := make([]int32, 0, len(i))
for _, index := range i {
indexes = append(indexes, int32(index))
}
m.EmptySegmentIndexes = indexes
}
}
// WithSkipSegmentIndexes sets which segment indexes should be skipped for the
// segments in the open month. If you use this option, you must either:
// 1. ensure that you've called Segment() for the open month
// 2. use WithMaximumSegmentIndex() to set the total number of segments
//
// If you haven't set either of those values then this option will be ignored,
// unless you included 0 as a skipped segment index in which case no segments
// will be created.
func WithSkipSegmentIndexes(i ...int) MonthOption {
return func(m *generation.Data) {
indexes := make([]int32, 0, len(i))
for _, index := range i {
indexes = append(indexes, int32(index))
}
m.SkipSegmentIndexes = indexes
}
}
// SetMonthOptions can be called at any time to set options for the open month
func (d *ActivityLogDataGenerator) SetMonthOptions(opts ...MonthOption) *ActivityLogDataGenerator {
for _, opt := range opts {
opt(d.addingToMonth)
}
return d
}
// ClientOption defines additional options for the client
// This type and the functions that return it are here for ease of use. A user
// could also choose to create the *generation.Client themselves, without using
// a ClientOption
type ClientOption func(client *generation.Client)
// WithClientNamespace sets the namespace for the client
func WithClientNamespace(n string) ClientOption {
return func(client *generation.Client) {
client.Namespace = n
}
}
// WithClientMount sets the mount path for the client
func WithClientMount(m string) ClientOption {
return func(client *generation.Client) {
client.Mount = m
}
}
// WithClientIsNonEntity sets whether the client is an entity client or a non-
// entity token client
func WithClientIsNonEntity() ClientOption {
return WithClientType("non-entity")
}
// WithClientType sets the client type to the given string. If this client type
// is not "entity", then the client will be counted in the activity log as a
// non-entity client
func WithClientType(typ string) ClientOption {
return func(client *generation.Client) {
client.ClientType = typ
}
}
// WithClientID sets the ID for the client
func WithClientID(id string) ClientOption {
return func(client *generation.Client) {
client.Id = id
}
}
// ClientsSeen adds clients to the month that was most recently opened with
// NewPreviousMonthData or NewCurrentMonthData.
func (d *ActivityLogDataGenerator) ClientsSeen(clients ...*generation.Client) *ActivityLogDataGenerator {
if d.addingToSegment == nil {
if d.addingToMonth.Clients == nil {
d.addingToMonth.Clients = &generation.Data_All{All: &generation.Clients{}}
}
d.addingToMonth.GetAll().Clients = append(d.addingToMonth.GetAll().Clients, clients...)
return d
}
d.addingToSegment.Clients.Clients = append(d.addingToSegment.Clients.Clients, clients...)
return d
}
// NewClientSeen adds 1 new client with the given options to the most recently
// opened month.
func (d *ActivityLogDataGenerator) NewClientSeen(opts ...ClientOption) *ActivityLogDataGenerator {
return d.NewClientsSeen(1, opts...)
}
// NewClientsSeen adds n new clients with the given options to the most recently
// opened month.
func (d *ActivityLogDataGenerator) NewClientsSeen(n int, opts ...ClientOption) *ActivityLogDataGenerator {
c := new(generation.Client)
for _, opt := range opts {
opt(c)
}
c.Count = int32(n)
return d.ClientsSeen(c)
}
// RepeatedClientSeen adds 1 client that was seen in the previous month to
// the month that was most recently opened. This client will have the attributes
// described by the provided options.
func (d *ActivityLogDataGenerator) RepeatedClientSeen(opts ...ClientOption) *ActivityLogDataGenerator {
return d.RepeatedClientsSeen(1, opts...)
}
// RepeatedClientsSeen adds n clients that were seen in the previous month to
// the month that was most recently opened. These clients will have the
// attributes described by provided options.
func (d *ActivityLogDataGenerator) RepeatedClientsSeen(n int, opts ...ClientOption) *ActivityLogDataGenerator {
c := new(generation.Client)
for _, opt := range opts {
opt(c)
}
c.Repeated = true
c.Count = int32(n)
return d.ClientsSeen(c)
}
// RepeatedClientSeenFromMonthsAgo adds 1 client that was seen in monthsAgo
// month to the month that was most recently opened. This client will have the
// attributes described by provided options.
func (d *ActivityLogDataGenerator) RepeatedClientSeenFromMonthsAgo(monthsAgo int, opts ...ClientOption) *ActivityLogDataGenerator {
return d.RepeatedClientsSeenFromMonthsAgo(1, monthsAgo, opts...)
}
// RepeatedClientsSeenFromMonthsAgo adds n clients that were seen in monthsAgo
// month to the month that was most recently opened. These clients will have the
// attributes described by provided options.
func (d *ActivityLogDataGenerator) RepeatedClientsSeenFromMonthsAgo(n, monthsAgo int, opts ...ClientOption) *ActivityLogDataGenerator {
c := new(generation.Client)
for _, opt := range opts {
opt(c)
}
c.RepeatedFromMonth = int32(monthsAgo)
c.Count = int32(n)
return d.ClientsSeen(c)
}
// SegmentOption defines additional options for the segment
type SegmentOption func(segment *generation.Segment)
// WithSegmentIndex sets the index for the segment to n. If this option is not
// provided, the segment will be given the next consecutive index
func WithSegmentIndex(n int) SegmentOption {
return func(segment *generation.Segment) {
index := int32(n)
segment.SegmentIndex = &index
}
}
// Segment starts a segment within the current month. All clients will be added
// to this segment, until either Segment is called again to create a new open
// segment, or NewPreviousMonthData or NewCurrentMonthData is called to open a
// new month.
func (d *ActivityLogDataGenerator) Segment(opts ...SegmentOption) *ActivityLogDataGenerator {
s := &generation.Segment{
Clients: &generation.Clients{},
}
for _, opt := range opts {
opt(s)
}
if d.addingToMonth.GetSegments() == nil {
d.addingToMonth.Clients = &generation.Data_Segments{Segments: &generation.Segments{}}
}
d.addingToMonth.GetSegments().Segments = append(d.addingToMonth.GetSegments().Segments, s)
d.addingToSegment = s
return d
}
// ToJSON returns the JSON representation of the data
func (d *ActivityLogDataGenerator) ToJSON() ([]byte, error) {
return protojson.Marshal(d.data)
}
// ToProto returns the ActivityLogMockInput protobuf
func (d *ActivityLogDataGenerator) ToProto() *generation.ActivityLogMockInput {
return d.data
}
// Write writes the data to the API with the given write options. The method
// returns the new paths that have been written. Note that the API endpoint will
// only be present when Vault has been compiled with the "testonly" flag.
func (d *ActivityLogDataGenerator) Write(ctx context.Context, writeOptions ...generation.WriteOptions) ([]string, error) {
d.data.Write = writeOptions
err := VerifyInput(d.data)
if err != nil {
return nil, err
}
data, err := d.ToJSON()
if err != nil {
return nil, err
}
resp, err := d.client.Logical().WriteWithContext(ctx, "sys/internal/counters/activity/write", map[string]interface{}{"input": string(data)})
if err != nil {
return nil, err
}
if resp.Data == nil {
return nil, fmt.Errorf("received no data")
}
paths := resp.Data["paths"]
castedPaths, ok := paths.([]interface{})
if !ok {
return nil, fmt.Errorf("invalid paths data: %v", paths)
}
returnPaths := make([]string, 0, len(castedPaths))
for _, path := range castedPaths {
returnPaths = append(returnPaths, path.(string))
}
return returnPaths, nil
}
// VerifyInput checks that the input data is valid
func VerifyInput(input *generation.ActivityLogMockInput) error {
// mapping from monthsAgo to the month's data
months := make(map[int32]*generation.Data)
// this keeps track of the index of the earliest month. We need to verify
// that this month doesn't have any repeated clients
earliestMonthsAgo := int32(0)
// this map holds a set of the month indexes for any RepeatedFromMonth
// values. Each element will be checked to ensure month that should be
// repeated from exists in the input data
repeatedFromMonths := make(map[int32]struct{})
for _, month := range input.Data {
monthsAgo := month.GetMonthsAgo()
if monthsAgo > earliestMonthsAgo {
earliestMonthsAgo = monthsAgo
}
// verify that no monthsAgo value is repeated
if _, seen := months[monthsAgo]; seen {
return fmt.Errorf("multiple months with monthsAgo %d", monthsAgo)
}
months[monthsAgo] = month
// the number of segments should be correct
if month.NumSegments > 0 && int(month.NumSegments)-len(month.GetSkipSegmentIndexes())-len(month.GetEmptySegmentIndexes()) <= 0 {
return fmt.Errorf("number of segments %d is too small. It must be large enough to include the empty (%v) and skipped (%v) segments", month.NumSegments, month.GetSkipSegmentIndexes(), month.GetEmptySegmentIndexes())
}
if segments := month.GetSegments(); segments != nil {
if month.NumSegments > 0 {
return errors.New("cannot specify both number of segments and create segmented data")
}
segmentIndexes := make(map[int32]struct{})
for _, segment := range segments.Segments {
// collect any RepeatedFromMonth values
for _, client := range segment.GetClients().GetClients() {
if repeatFrom := client.RepeatedFromMonth; repeatFrom > 0 {
repeatedFromMonths[repeatFrom] = struct{}{}
}
}
// verify that no segment indexes are repeated
segmentIndex := segment.SegmentIndex
if segmentIndex == nil {
continue
}
if _, seen := segmentIndexes[*segmentIndex]; seen {
return fmt.Errorf("cannot have repeated segment index %d", *segmentIndex)
}
segmentIndexes[*segmentIndex] = struct{}{}
}
} else {
for _, client := range month.GetAll().GetClients() {
// collect any RepeatedFromMonth values
if repeatFrom := client.RepeatedFromMonth; repeatFrom > 0 {
repeatedFromMonths[repeatFrom] = struct{}{}
}
}
}
}
// check that the corresponding month exists for all the RepeatedFromMonth
// values
for repeated := range repeatedFromMonths {
if _, ok := months[repeated]; !ok {
return fmt.Errorf("cannot repeat from %d months ago", repeated)
}
}
// the earliest month can't have any repeated clients, because there are no
// earlier months to repeat from
earliestMonth := months[earliestMonthsAgo]
repeatedClients := false
if all := earliestMonth.GetAll(); all != nil {
for _, client := range all.GetClients() {
repeatedClients = repeatedClients || client.Repeated || client.RepeatedFromMonth != 0
}
} else {
for _, segment := range earliestMonth.GetSegments().GetSegments() {
for _, client := range segment.GetClients().GetClients() {
repeatedClients = repeatedClients || client.Repeated || client.RepeatedFromMonth != 0
}
}
}
if repeatedClients {
return fmt.Errorf("%d months ago cannot have repeated clients, because it is the earliest month", earliestMonthsAgo)
}
return nil
}