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 }