github.com/hashicorp/vault/sdk@v0.13.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(writeOptions ...generation.WriteOptions) ([]byte, error) {
   271  	if len(writeOptions) > 0 {
   272  		d.data.Write = writeOptions
   273  	}
   274  	return protojson.Marshal(d.data)
   275  }
   276  
   277  // ToProto returns the ActivityLogMockInput protobuf
   278  func (d *ActivityLogDataGenerator) ToProto() *generation.ActivityLogMockInput {
   279  	return d.data
   280  }
   281  
   282  // Write writes the data to the API with the given write options. The method
   283  // returns the new paths that have been written. Note that the API endpoint will
   284  // only be present when Vault has been compiled with the "testonly" flag.
   285  func (d *ActivityLogDataGenerator) Write(ctx context.Context, writeOptions ...generation.WriteOptions) ([]string, error) {
   286  	d.data.Write = writeOptions
   287  	err := VerifyInput(d.data)
   288  	if err != nil {
   289  		return nil, err
   290  	}
   291  	data, err := d.ToJSON()
   292  	if err != nil {
   293  		return nil, err
   294  	}
   295  	resp, err := d.client.Logical().WriteWithContext(ctx, "sys/internal/counters/activity/write", map[string]interface{}{"input": string(data)})
   296  	if err != nil {
   297  		return nil, err
   298  	}
   299  	if resp.Data == nil {
   300  		return nil, fmt.Errorf("received no data")
   301  	}
   302  	paths := resp.Data["paths"]
   303  	castedPaths, ok := paths.([]interface{})
   304  	if !ok {
   305  		return nil, fmt.Errorf("invalid paths data: %v", paths)
   306  	}
   307  	returnPaths := make([]string, 0, len(castedPaths))
   308  	for _, path := range castedPaths {
   309  		returnPaths = append(returnPaths, path.(string))
   310  	}
   311  	return returnPaths, nil
   312  }
   313  
   314  // VerifyInput checks that the input data is valid
   315  func VerifyInput(input *generation.ActivityLogMockInput) error {
   316  	// mapping from monthsAgo to the month's data
   317  	months := make(map[int32]*generation.Data)
   318  
   319  	// this keeps track of the index of the earliest month. We need to verify
   320  	// that this month doesn't have any repeated clients
   321  	earliestMonthsAgo := int32(0)
   322  
   323  	// this map holds a set of the month indexes for any RepeatedFromMonth
   324  	// values. Each element will be checked to ensure month that should be
   325  	// repeated from exists in the input data
   326  	repeatedFromMonths := make(map[int32]struct{})
   327  
   328  	for _, month := range input.Data {
   329  		monthsAgo := month.GetMonthsAgo()
   330  		if monthsAgo > earliestMonthsAgo {
   331  			earliestMonthsAgo = monthsAgo
   332  		}
   333  
   334  		// verify that no monthsAgo value is repeated
   335  		if _, seen := months[monthsAgo]; seen {
   336  			return fmt.Errorf("multiple months with monthsAgo %d", monthsAgo)
   337  		}
   338  		months[monthsAgo] = month
   339  
   340  		// the number of segments should be correct
   341  		if month.NumSegments > 0 && int(month.NumSegments)-len(month.GetSkipSegmentIndexes())-len(month.GetEmptySegmentIndexes()) <= 0 {
   342  			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())
   343  		}
   344  
   345  		if segments := month.GetSegments(); segments != nil {
   346  			if month.NumSegments > 0 {
   347  				return errors.New("cannot specify both number of segments and create segmented data")
   348  			}
   349  
   350  			segmentIndexes := make(map[int32]struct{})
   351  			for _, segment := range segments.Segments {
   352  
   353  				// collect any RepeatedFromMonth values
   354  				for _, client := range segment.GetClients().GetClients() {
   355  					if repeatFrom := client.RepeatedFromMonth; repeatFrom > 0 {
   356  						repeatedFromMonths[repeatFrom] = struct{}{}
   357  					}
   358  				}
   359  
   360  				// verify that no segment indexes are repeated
   361  				segmentIndex := segment.SegmentIndex
   362  				if segmentIndex == nil {
   363  					continue
   364  				}
   365  				if _, seen := segmentIndexes[*segmentIndex]; seen {
   366  					return fmt.Errorf("cannot have repeated segment index %d", *segmentIndex)
   367  				}
   368  				segmentIndexes[*segmentIndex] = struct{}{}
   369  			}
   370  		} else {
   371  			for _, client := range month.GetAll().GetClients() {
   372  				// collect any RepeatedFromMonth values
   373  				if repeatFrom := client.RepeatedFromMonth; repeatFrom > 0 {
   374  					repeatedFromMonths[repeatFrom] = struct{}{}
   375  				}
   376  			}
   377  		}
   378  	}
   379  
   380  	// check that the corresponding month exists for all the RepeatedFromMonth
   381  	// values
   382  	for repeated := range repeatedFromMonths {
   383  		if _, ok := months[repeated]; !ok {
   384  			return fmt.Errorf("cannot repeat from %d months ago", repeated)
   385  		}
   386  	}
   387  	// the earliest month can't have any repeated clients, because there are no
   388  	// earlier months to repeat from
   389  	earliestMonth := months[earliestMonthsAgo]
   390  	repeatedClients := false
   391  	if all := earliestMonth.GetAll(); all != nil {
   392  		for _, client := range all.GetClients() {
   393  			repeatedClients = repeatedClients || client.Repeated || client.RepeatedFromMonth != 0
   394  		}
   395  	} else {
   396  		for _, segment := range earliestMonth.GetSegments().GetSegments() {
   397  			for _, client := range segment.GetClients().GetClients() {
   398  				repeatedClients = repeatedClients || client.Repeated || client.RepeatedFromMonth != 0
   399  			}
   400  		}
   401  	}
   402  
   403  	if repeatedClients {
   404  		return fmt.Errorf("%d months ago cannot have repeated clients, because it is the earliest month", earliestMonthsAgo)
   405  	}
   406  
   407  	return nil
   408  }