github.com/hashicorp/vault/sdk@v0.11.0/helper/clientcountutil/clientcountutil.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  // Package clientcountutil provides a library to generate activity log data for
     5  // testing.
     6  package clientcountutil
     7  
     8  import (
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  
    13  	"github.com/hashicorp/vault/api"
    14  	"github.com/hashicorp/vault/sdk/helper/clientcountutil/generation"
    15  	"google.golang.org/protobuf/encoding/protojson"
    16  )
    17  
    18  // ActivityLogDataGenerator holds an ActivityLogMockInput. Users can create the
    19  // generator with NewActivityLogData(), add content to the generator using
    20  // the fluent API methods, and generate and write the JSON representation of the
    21  // input to the Vault API.
    22  type ActivityLogDataGenerator struct {
    23  	data            *generation.ActivityLogMockInput
    24  	addingToMonth   *generation.Data
    25  	addingToSegment *generation.Segment
    26  	client          *api.Client
    27  }
    28  
    29  // NewActivityLogData creates a new instance of an activity log data generator
    30  // The type returned by this function cannot be called concurrently
    31  func NewActivityLogData(client *api.Client) *ActivityLogDataGenerator {
    32  	return &ActivityLogDataGenerator{
    33  		client: client,
    34  		data:   new(generation.ActivityLogMockInput),
    35  	}
    36  }
    37  
    38  // NewCurrentMonthData opens a new month of data for the current month. All
    39  // clients will continue to be added to this month until a new month is created
    40  // with NewPreviousMonthData.
    41  func (d *ActivityLogDataGenerator) NewCurrentMonthData() *ActivityLogDataGenerator {
    42  	return d.newMonth(&generation.Data{Month: &generation.Data_CurrentMonth{CurrentMonth: true}})
    43  }
    44  
    45  // NewPreviousMonthData opens a new month of data, where the clients will be
    46  // recorded as having been seen monthsAgo months ago. All clients will continue
    47  // to be added to this month until a new month is created with
    48  // NewPreviousMonthData or NewCurrentMonthData.
    49  func (d *ActivityLogDataGenerator) NewPreviousMonthData(monthsAgo int) *ActivityLogDataGenerator {
    50  	return d.newMonth(&generation.Data{Month: &generation.Data_MonthsAgo{MonthsAgo: int32(monthsAgo)}})
    51  }
    52  
    53  func (d *ActivityLogDataGenerator) newMonth(newMonth *generation.Data) *ActivityLogDataGenerator {
    54  	d.data.Data = append(d.data.Data, newMonth)
    55  	d.addingToMonth = newMonth
    56  	d.addingToSegment = nil
    57  	return d
    58  }
    59  
    60  // MonthOption holds an option that can be set for the entire month
    61  type MonthOption func(m *generation.Data)
    62  
    63  // WithMaximumSegmentIndex sets the maximum segment index for the segments in
    64  // the open month. Set this value in order to set how many indexes the data
    65  // should be split across. This must include any empty or skipped indexes. For
    66  // example, say that you would like all of your data split across indexes 0 and
    67  // 3, with the following empty and skipped indexes:
    68  //
    69  //	empty indexes: [2]
    70  //	skipped indexes: [1]
    71  //
    72  // To accomplish that, you will need to call WithMaximumSegmentIndex(3).
    73  // This value will be ignored if you have called Segment() for the open month
    74  // If not set, all data will be in 1 segment.
    75  func WithMaximumSegmentIndex(n int) MonthOption {
    76  	return func(m *generation.Data) {
    77  		m.NumSegments = int32(n)
    78  	}
    79  }
    80  
    81  // WithEmptySegmentIndexes sets which segment indexes should be empty for the
    82  // segments in the open month. If you use this option, you must either:
    83  //  1. ensure that you've called Segment() for the open month
    84  //  2. use WithMaximumSegmentIndex() to set the total number of segments
    85  //
    86  // If you haven't set either of those values then this option will be ignored,
    87  // unless you included 0 as an empty segment index in which case only an empty
    88  // segment will be created.
    89  func WithEmptySegmentIndexes(i ...int) MonthOption {
    90  	return func(m *generation.Data) {
    91  		indexes := make([]int32, 0, len(i))
    92  		for _, index := range i {
    93  			indexes = append(indexes, int32(index))
    94  		}
    95  		m.EmptySegmentIndexes = indexes
    96  	}
    97  }
    98  
    99  // WithSkipSegmentIndexes sets which segment indexes should be skipped for the
   100  // segments in the open month. If you use this option, you must either:
   101  //  1. ensure that you've called Segment() for the open month
   102  //  2. use WithMaximumSegmentIndex() to set the total number of segments
   103  //
   104  // If you haven't set either of those values then this option will be ignored,
   105  // unless you included 0 as a skipped segment index in which case no segments
   106  // will be created.
   107  func WithSkipSegmentIndexes(i ...int) MonthOption {
   108  	return func(m *generation.Data) {
   109  		indexes := make([]int32, 0, len(i))
   110  		for _, index := range i {
   111  			indexes = append(indexes, int32(index))
   112  		}
   113  		m.SkipSegmentIndexes = indexes
   114  	}
   115  }
   116  
   117  // SetMonthOptions can be called at any time to set options for the open month
   118  func (d *ActivityLogDataGenerator) SetMonthOptions(opts ...MonthOption) *ActivityLogDataGenerator {
   119  	for _, opt := range opts {
   120  		opt(d.addingToMonth)
   121  	}
   122  	return d
   123  }
   124  
   125  // ClientOption defines additional options for the client
   126  // This type and the functions that return it are here for ease of use. A user
   127  // could also choose to create the *generation.Client themselves, without using
   128  // a ClientOption
   129  type ClientOption func(client *generation.Client)
   130  
   131  // WithClientNamespace sets the namespace for the client
   132  func WithClientNamespace(n string) ClientOption {
   133  	return func(client *generation.Client) {
   134  		client.Namespace = n
   135  	}
   136  }
   137  
   138  // WithClientMount sets the mount path for the client
   139  func WithClientMount(m string) ClientOption {
   140  	return func(client *generation.Client) {
   141  		client.Mount = m
   142  	}
   143  }
   144  
   145  // WithClientIsNonEntity sets whether the client is an entity client or a non-
   146  // entity token client
   147  func WithClientIsNonEntity() ClientOption {
   148  	return WithClientType("non-entity")
   149  }
   150  
   151  // WithClientType sets the client type to the given string. If this client type
   152  // is not "entity", then the client will be counted in the activity log as a
   153  // non-entity client
   154  func WithClientType(typ string) ClientOption {
   155  	return func(client *generation.Client) {
   156  		client.ClientType = typ
   157  	}
   158  }
   159  
   160  // WithClientID sets the ID for the client
   161  func WithClientID(id string) ClientOption {
   162  	return func(client *generation.Client) {
   163  		client.Id = id
   164  	}
   165  }
   166  
   167  // ClientsSeen adds clients to the month that was most recently opened with
   168  // NewPreviousMonthData or NewCurrentMonthData.
   169  func (d *ActivityLogDataGenerator) ClientsSeen(clients ...*generation.Client) *ActivityLogDataGenerator {
   170  	if d.addingToSegment == nil {
   171  		if d.addingToMonth.Clients == nil {
   172  			d.addingToMonth.Clients = &generation.Data_All{All: &generation.Clients{}}
   173  		}
   174  		d.addingToMonth.GetAll().Clients = append(d.addingToMonth.GetAll().Clients, clients...)
   175  		return d
   176  	}
   177  	d.addingToSegment.Clients.Clients = append(d.addingToSegment.Clients.Clients, clients...)
   178  	return d
   179  }
   180  
   181  // NewClientSeen adds 1 new client with the given options to the most recently
   182  // opened month.
   183  func (d *ActivityLogDataGenerator) NewClientSeen(opts ...ClientOption) *ActivityLogDataGenerator {
   184  	return d.NewClientsSeen(1, opts...)
   185  }
   186  
   187  // NewClientsSeen adds n new clients with the given options to the most recently
   188  // opened month.
   189  func (d *ActivityLogDataGenerator) NewClientsSeen(n int, opts ...ClientOption) *ActivityLogDataGenerator {
   190  	c := new(generation.Client)
   191  	for _, opt := range opts {
   192  		opt(c)
   193  	}
   194  	c.Count = int32(n)
   195  	return d.ClientsSeen(c)
   196  }
   197  
   198  // RepeatedClientSeen adds 1 client that was seen in the previous month to
   199  // the month that was most recently opened. This client will have the attributes
   200  // described by the provided options.
   201  func (d *ActivityLogDataGenerator) RepeatedClientSeen(opts ...ClientOption) *ActivityLogDataGenerator {
   202  	return d.RepeatedClientsSeen(1, opts...)
   203  }
   204  
   205  // RepeatedClientsSeen adds n clients that were seen in the previous month to
   206  // the month that was most recently opened. These clients will have the
   207  // attributes described by provided options.
   208  func (d *ActivityLogDataGenerator) RepeatedClientsSeen(n int, opts ...ClientOption) *ActivityLogDataGenerator {
   209  	c := new(generation.Client)
   210  	for _, opt := range opts {
   211  		opt(c)
   212  	}
   213  	c.Repeated = true
   214  	c.Count = int32(n)
   215  	return d.ClientsSeen(c)
   216  }
   217  
   218  // RepeatedClientSeenFromMonthsAgo adds 1 client that was seen in monthsAgo
   219  // month to the month that was most recently opened. This client will have the
   220  // attributes described by provided options.
   221  func (d *ActivityLogDataGenerator) RepeatedClientSeenFromMonthsAgo(monthsAgo int, opts ...ClientOption) *ActivityLogDataGenerator {
   222  	return d.RepeatedClientsSeenFromMonthsAgo(1, monthsAgo, opts...)
   223  }
   224  
   225  // RepeatedClientsSeenFromMonthsAgo adds n clients that were seen in monthsAgo
   226  // month to the month that was most recently opened. These clients will have the
   227  // attributes described by provided options.
   228  func (d *ActivityLogDataGenerator) RepeatedClientsSeenFromMonthsAgo(n, monthsAgo int, opts ...ClientOption) *ActivityLogDataGenerator {
   229  	c := new(generation.Client)
   230  	for _, opt := range opts {
   231  		opt(c)
   232  	}
   233  	c.RepeatedFromMonth = int32(monthsAgo)
   234  	c.Count = int32(n)
   235  	return d.ClientsSeen(c)
   236  }
   237  
   238  // SegmentOption defines additional options for the segment
   239  type SegmentOption func(segment *generation.Segment)
   240  
   241  // WithSegmentIndex sets the index for the segment to n. If this option is not
   242  // provided, the segment will be given the next consecutive index
   243  func WithSegmentIndex(n int) SegmentOption {
   244  	return func(segment *generation.Segment) {
   245  		index := int32(n)
   246  		segment.SegmentIndex = &index
   247  	}
   248  }
   249  
   250  // Segment starts a segment within the current month. All clients will be added
   251  // to this segment, until either Segment is called again to create a new open
   252  // segment, or NewPreviousMonthData or NewCurrentMonthData is called to open a
   253  // new month.
   254  func (d *ActivityLogDataGenerator) Segment(opts ...SegmentOption) *ActivityLogDataGenerator {
   255  	s := &generation.Segment{
   256  		Clients: &generation.Clients{},
   257  	}
   258  	for _, opt := range opts {
   259  		opt(s)
   260  	}
   261  	if d.addingToMonth.GetSegments() == nil {
   262  		d.addingToMonth.Clients = &generation.Data_Segments{Segments: &generation.Segments{}}
   263  	}
   264  	d.addingToMonth.GetSegments().Segments = append(d.addingToMonth.GetSegments().Segments, s)
   265  	d.addingToSegment = s
   266  	return d
   267  }
   268  
   269  // ToJSON returns the JSON representation of the data
   270  func (d *ActivityLogDataGenerator) ToJSON() ([]byte, error) {
   271  	return protojson.Marshal(d.data)
   272  }
   273  
   274  // ToProto returns the ActivityLogMockInput protobuf
   275  func (d *ActivityLogDataGenerator) ToProto() *generation.ActivityLogMockInput {
   276  	return d.data
   277  }
   278  
   279  // Write writes the data to the API with the given write options. The method
   280  // returns the new paths that have been written. Note that the API endpoint will
   281  // only be present when Vault has been compiled with the "testonly" flag.
   282  func (d *ActivityLogDataGenerator) Write(ctx context.Context, writeOptions ...generation.WriteOptions) ([]string, error) {
   283  	d.data.Write = writeOptions
   284  	err := VerifyInput(d.data)
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  	data, err := d.ToJSON()
   289  	if err != nil {
   290  		return nil, err
   291  	}
   292  	resp, err := d.client.Logical().WriteWithContext(ctx, "sys/internal/counters/activity/write", map[string]interface{}{"input": string(data)})
   293  	if err != nil {
   294  		return nil, err
   295  	}
   296  	if resp.Data == nil {
   297  		return nil, fmt.Errorf("received no data")
   298  	}
   299  	paths := resp.Data["paths"]
   300  	castedPaths, ok := paths.([]interface{})
   301  	if !ok {
   302  		return nil, fmt.Errorf("invalid paths data: %v", paths)
   303  	}
   304  	returnPaths := make([]string, 0, len(castedPaths))
   305  	for _, path := range castedPaths {
   306  		returnPaths = append(returnPaths, path.(string))
   307  	}
   308  	return returnPaths, nil
   309  }
   310  
   311  // VerifyInput checks that the input data is valid
   312  func VerifyInput(input *generation.ActivityLogMockInput) error {
   313  	// mapping from monthsAgo to the month's data
   314  	months := make(map[int32]*generation.Data)
   315  
   316  	// this keeps track of the index of the earliest month. We need to verify
   317  	// that this month doesn't have any repeated clients
   318  	earliestMonthsAgo := int32(0)
   319  
   320  	// this map holds a set of the month indexes for any RepeatedFromMonth
   321  	// values. Each element will be checked to ensure month that should be
   322  	// repeated from exists in the input data
   323  	repeatedFromMonths := make(map[int32]struct{})
   324  
   325  	for _, month := range input.Data {
   326  		monthsAgo := month.GetMonthsAgo()
   327  		if monthsAgo > earliestMonthsAgo {
   328  			earliestMonthsAgo = monthsAgo
   329  		}
   330  
   331  		// verify that no monthsAgo value is repeated
   332  		if _, seen := months[monthsAgo]; seen {
   333  			return fmt.Errorf("multiple months with monthsAgo %d", monthsAgo)
   334  		}
   335  		months[monthsAgo] = month
   336  
   337  		// the number of segments should be correct
   338  		if month.NumSegments > 0 && int(month.NumSegments)-len(month.GetSkipSegmentIndexes())-len(month.GetEmptySegmentIndexes()) <= 0 {
   339  			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())
   340  		}
   341  
   342  		if segments := month.GetSegments(); segments != nil {
   343  			if month.NumSegments > 0 {
   344  				return errors.New("cannot specify both number of segments and create segmented data")
   345  			}
   346  
   347  			segmentIndexes := make(map[int32]struct{})
   348  			for _, segment := range segments.Segments {
   349  
   350  				// collect any RepeatedFromMonth values
   351  				for _, client := range segment.GetClients().GetClients() {
   352  					if repeatFrom := client.RepeatedFromMonth; repeatFrom > 0 {
   353  						repeatedFromMonths[repeatFrom] = struct{}{}
   354  					}
   355  				}
   356  
   357  				// verify that no segment indexes are repeated
   358  				segmentIndex := segment.SegmentIndex
   359  				if segmentIndex == nil {
   360  					continue
   361  				}
   362  				if _, seen := segmentIndexes[*segmentIndex]; seen {
   363  					return fmt.Errorf("cannot have repeated segment index %d", *segmentIndex)
   364  				}
   365  				segmentIndexes[*segmentIndex] = struct{}{}
   366  			}
   367  		} else {
   368  			for _, client := range month.GetAll().GetClients() {
   369  				// collect any RepeatedFromMonth values
   370  				if repeatFrom := client.RepeatedFromMonth; repeatFrom > 0 {
   371  					repeatedFromMonths[repeatFrom] = struct{}{}
   372  				}
   373  			}
   374  		}
   375  	}
   376  
   377  	// check that the corresponding month exists for all the RepeatedFromMonth
   378  	// values
   379  	for repeated := range repeatedFromMonths {
   380  		if _, ok := months[repeated]; !ok {
   381  			return fmt.Errorf("cannot repeat from %d months ago", repeated)
   382  		}
   383  	}
   384  	// the earliest month can't have any repeated clients, because there are no
   385  	// earlier months to repeat from
   386  	earliestMonth := months[earliestMonthsAgo]
   387  	repeatedClients := false
   388  	if all := earliestMonth.GetAll(); all != nil {
   389  		for _, client := range all.GetClients() {
   390  			repeatedClients = repeatedClients || client.Repeated || client.RepeatedFromMonth != 0
   391  		}
   392  	} else {
   393  		for _, segment := range earliestMonth.GetSegments().GetSegments() {
   394  			for _, client := range segment.GetClients().GetClients() {
   395  				repeatedClients = repeatedClients || client.Repeated || client.RepeatedFromMonth != 0
   396  			}
   397  		}
   398  	}
   399  
   400  	if repeatedClients {
   401  		return fmt.Errorf("%d months ago cannot have repeated clients, because it is the earliest month", earliestMonthsAgo)
   402  	}
   403  
   404  	return nil
   405  }