99 "encoding/json"
1010 "errors"
1111 "fmt"
12+ "math"
1213 "net/http"
1314 "os"
1415 "path/filepath"
@@ -25,6 +26,7 @@ import (
2526 "github.com/go-test/deep"
2627 "github.com/golang/protobuf/proto"
2728 "github.com/hashicorp/go-uuid"
29+ "github.com/hashicorp/vault/builtin/credential/userpass"
2830 "github.com/hashicorp/vault/helper/constants"
2931 "github.com/hashicorp/vault/helper/namespace"
3032 "github.com/hashicorp/vault/helper/timeutil"
@@ -5084,3 +5086,193 @@ func TestActivityLog_reportPrecomputedQueryMetrics(t *testing.T) {
50845086 hasMetric (t , data , "identity.pki_acme.active.reporting_period" , 3 , nil )
50855087 })
50865088}
5089+
5090+ // TestHandleQuery_MultipleMounts creates a cluster with
5091+ // two userpass mounts. It then tests verifies that
5092+ // the total new counts are calculated within a reasonably level of accuracy for
5093+ // various numbers of clients in each mount.
5094+ func TestHandleQuery_MultipleMounts (t * testing.T ) {
5095+ tests := map [string ]struct {
5096+ twoMonthsAgo [][]int
5097+ oneMonthAgo [][]int
5098+ currentMonth [][]int
5099+ expectedNewClients int
5100+ repeatPreviousMonth bool
5101+ expectedTotalAccuracy float64
5102+ }{
5103+ "low volume, all mounts" : {
5104+ twoMonthsAgo : [][]int {
5105+ {20 , 20 },
5106+ },
5107+ oneMonthAgo : [][]int {
5108+ {30 , 30 },
5109+ },
5110+ currentMonth : [][]int {
5111+ {40 , 40 },
5112+ },
5113+ repeatPreviousMonth : true ,
5114+ expectedNewClients : 80 ,
5115+ expectedTotalAccuracy : 1 ,
5116+ },
5117+ "medium volume, all mounts" : {
5118+ twoMonthsAgo : [][]int {
5119+ {200 , 200 },
5120+ },
5121+ oneMonthAgo : [][]int {
5122+ {300 , 300 },
5123+ },
5124+ currentMonth : [][]int {
5125+ {400 , 400 },
5126+ },
5127+ repeatPreviousMonth : true ,
5128+ expectedNewClients : 800 ,
5129+ expectedTotalAccuracy : 0.98 ,
5130+ },
5131+ "higher volume, all mounts" : {
5132+ twoMonthsAgo : [][]int {
5133+ {200 , 200 },
5134+ },
5135+ oneMonthAgo : [][]int {
5136+ {300 , 300 },
5137+ },
5138+ currentMonth : [][]int {
5139+ {2000 , 5000 },
5140+ },
5141+ repeatPreviousMonth : true ,
5142+ expectedNewClients : 7000 ,
5143+ expectedTotalAccuracy : 0.95 ,
5144+ },
5145+ "higher volume, no repeats" : {
5146+ twoMonthsAgo : [][]int {
5147+ {200 , 200 },
5148+ },
5149+ oneMonthAgo : [][]int {
5150+ {300 , 300 },
5151+ },
5152+ currentMonth : [][]int {
5153+ {4000 , 6000 },
5154+ },
5155+ repeatPreviousMonth : false ,
5156+ expectedNewClients : 10000 ,
5157+ expectedTotalAccuracy : 0.98 ,
5158+ },
5159+ }
5160+
5161+ for i , tt := range tests {
5162+ testname := fmt .Sprintf ("%s" , i )
5163+ t .Run (testname , func (t * testing.T ) {
5164+ // Normalize to start of month to prevent end of month weirdness
5165+ startOfMonth := timeutil .StartOfMonth (time .Now ().UTC ())
5166+
5167+ storage := & logical.InmemStorage {}
5168+ coreConfig := & CoreConfig {
5169+ CredentialBackends : map [string ]logical.Factory {
5170+ "userpass" : userpass .Factory ,
5171+ },
5172+ Physical : storage .Underlying (),
5173+ }
5174+
5175+ cluster := NewTestCluster (t , coreConfig , nil )
5176+ cluster .Start ()
5177+ defer cluster .Cleanup ()
5178+ core := cluster .Cores [0 ].Core
5179+ TestWaitActive (t , core )
5180+
5181+ a := core .activityLog
5182+ ctx := namespace .RootContext (nil )
5183+ var err error
5184+
5185+ namespaces := make ([]* namespace.Namespace , 0 , 6 )
5186+ mounts := make (map [string ][]* MountEntry )
5187+ ns := namespace .RootNamespace
5188+ namespaces = append (namespaces , ns )
5189+
5190+ // Add two userpass mounts to the root namespace
5191+ for i := 0 ; i < 1 ; i ++ {
5192+ me1 := & MountEntry {
5193+ Table : credentialTableType ,
5194+ Path : "up1/" ,
5195+ Type : "userpass" ,
5196+ }
5197+ err = core .enableCredential (namespace .ContextWithNamespace (ctx , namespaces [i ]), me1 )
5198+ require .NoError (t , err )
5199+
5200+ me2 := & MountEntry {
5201+ Table : credentialTableType ,
5202+ Path : "up2/" ,
5203+ Type : "userpass" ,
5204+ }
5205+ err = core .enableCredential (namespace .ContextWithNamespace (ctx , namespaces [i ]), me2 )
5206+ require .NoError (t , err )
5207+ mounts [namespaces [i ].ID ] = []* MountEntry {me1 , me2 }
5208+ }
5209+
5210+ // Generate data for two months ago
5211+ clientPrefix := 1
5212+ var entityRecordsMonth2 []* activity.EntityRecord
5213+ for namespaceId , mountSlice := range mounts {
5214+ for mountIndex , mount := range mountSlice {
5215+ entityRecordsMonth2 = append (entityRecordsMonth2 , generateClientData (0 , tt .twoMonthsAgo [0 ][mountIndex ], mount .Accessor , namespaceId , fmt .Sprintf ("%d" , clientPrefix ))... )
5216+ }
5217+ }
5218+
5219+ // Generate data for a month ago
5220+ clientPrefix += 1
5221+ var entityRecordsMonth1 []* activity.EntityRecord
5222+ for namespaceId , mountSlice := range mounts {
5223+ for mountIndex , mount := range mountSlice {
5224+ entityRecordsMonth2 = append (entityRecordsMonth1 , generateClientData (0 , tt .oneMonthAgo [0 ][mountIndex ], mount .Accessor , namespaceId , fmt .Sprintf ("%d" , clientPrefix ))... )
5225+ }
5226+ }
5227+
5228+ startOfTwoMonthsAgo := timeutil .StartOfMonth (startOfMonth .AddDate (0 , - 2 , 0 )).UTC ()
5229+ startOfOneMonthAgo := timeutil .StartOfMonth (startOfMonth .AddDate (0 , - 1 , 0 )).UTC ()
5230+ generatePreviousMonthClientData (t , core , startOfTwoMonthsAgo , entityRecordsMonth2 )
5231+ generatePreviousMonthClientData (t , core , startOfOneMonthAgo , entityRecordsMonth1 )
5232+
5233+ // Generate the current months data
5234+ clientPrefix += 1
5235+ var currentMonthData []* activity.EntityRecord
5236+ for namespaceId , mountSlice := range mounts {
5237+ for mountIndex , mount := range mountSlice {
5238+ currentMonthData = append (currentMonthData , generateClientData (0 , tt .currentMonth [0 ][mountIndex ], mount .Accessor , namespaceId , fmt .Sprintf ("%d" , clientPrefix ))... )
5239+ clientPrefix += 1
5240+ mountIndex ++
5241+ }
5242+ }
5243+ generateCurrentMonthClientData (t , core , entityRecordsMonth2 , currentMonthData )
5244+
5245+ endOfCurrentMonth := timeutil .EndOfMonth (time .Now ().UTC ())
5246+ actual , err := a .handleQuery (ctx , startOfTwoMonthsAgo , endOfCurrentMonth , 0 )
5247+
5248+ // Ensure that the month response is the same as the totals, because all clients
5249+ // are new clients and there will be no approximation in the single month partial
5250+ // case
5251+ monthsRaw , ok := actual ["months" ]
5252+ if ! ok {
5253+ t .Fatalf ("malformed results. got %v" , actual )
5254+ }
5255+ monthsResponse := make ([]ResponseMonth , 0 )
5256+ err = mapstructure .Decode (monthsRaw , & monthsResponse )
5257+
5258+ currentMonthClients := monthsResponse [len (monthsResponse )- 1 ]
5259+
5260+ // New verify that the new client totals for ALL namespaces are approximately accurate
5261+ newClientsError := math .Abs ((float64 )(currentMonthClients .NewClients .Counts .Clients - tt .expectedNewClients ))
5262+ newClientsErrorMargin := newClientsError / (float64 )(tt .expectedNewClients )
5263+ expectedAccuracyCalc := (1 - tt .expectedTotalAccuracy ) * 100 / 100
5264+ if newClientsErrorMargin > expectedAccuracyCalc {
5265+ t .Fatalf ("bad accuracy: expected %+v, found %+v" , expectedAccuracyCalc , newClientsErrorMargin )
5266+ }
5267+
5268+ // Verify that the totals for the clients are visibly sensible (that is the total of all the individual new clients per namespace)
5269+ total := 0
5270+ for _ , newClientCounts := range currentMonthClients .NewClients .Namespaces {
5271+ total += newClientCounts .Counts .Clients
5272+ }
5273+ if diff := math .Abs (float64 (currentMonthClients .NewClients .Counts .Clients - total )); diff >= 1 {
5274+ t .Fatalf ("total expected was %d but got %d" , currentMonthClients .NewClients .Counts .Clients , total )
5275+ }
5276+ })
5277+ }
5278+ }
0 commit comments