From 4f661c67c1b1b351097ec026792ea8bbf6a9e874 Mon Sep 17 00:00:00 2001 From: Amir Aslamov Date: Tue, 15 Apr 2025 09:36:14 -0400 Subject: [PATCH] VAULT-34674 (#30164) * apply oss changes * comment fix * fix TestOperatorUsageCommandRun by defining billing start via license * update go mod * revert the changes in operator usage test * fix operator usage test * fix acme regeneration tests * revert the changes for activity testonly test * fix activity testonly tests * seperate tests into ce and ent * move 2 more tests to oss and ent * remove left over test from common * updates after feedback * updates * added unit tests to tests oss get start and end time function * bring updates from ent * carry over updates from ent pr * fix the wording in ce warning * add a dot to ent warning * update comment * revert go mod and go sum changes, remove the unintended oss changes patch * add changelog entree for ce --- changelog/30164.txt | 3 + ...go => operator_usage_testonly_oss_test.go} | 10 +- vault/activity_log.go | 8 +- vault/activity_log_testonly_oss_test.go | 98 ++++ vault/activity_log_util.go | 22 + .../acme_regeneration_oss_test.go | 95 ++++ .../acme_regeneration_test.go | 66 --- .../activity_testonly_oss_test.go | 521 ++++++++++++++++++ .../activity_testonly_test.go | 479 ---------------- vault/logical_system_activity.go | 21 +- 10 files changed, 765 insertions(+), 558 deletions(-) create mode 100644 changelog/30164.txt rename command/command_testonly/{operator_usage_testonly_test.go => operator_usage_testonly_oss_test.go} (93%) create mode 100644 vault/external_tests/activity_testonly/acme_regeneration_oss_test.go diff --git a/changelog/30164.txt b/changelog/30164.txt new file mode 100644 index 0000000000..e17cfbdf64 --- /dev/null +++ b/changelog/30164.txt @@ -0,0 +1,3 @@ +```release-note:change +activity: provided value for `end_time` in `sys/internal/counters/activity` is now capped at the end of the last completed month. +``` \ No newline at end of file diff --git a/command/command_testonly/operator_usage_testonly_test.go b/command/command_testonly/operator_usage_testonly_oss_test.go similarity index 93% rename from command/command_testonly/operator_usage_testonly_test.go rename to command/command_testonly/operator_usage_testonly_oss_test.go index 31de4b88eb..1b805c31d5 100644 --- a/command/command_testonly/operator_usage_testonly_test.go +++ b/command/command_testonly/operator_usage_testonly_oss_test.go @@ -1,7 +1,7 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 -//go:build testonly +//go:build testonly && !enterprise package command_testonly @@ -54,12 +54,12 @@ func TestOperatorUsageCommandRun(t *testing.T) { now := time.Now().UTC() _, err = clientcountutil.NewActivityLogData(client). - NewPreviousMonthData(1). + NewPreviousMonthData(2). NewClientsSeen(6, clientcountutil.WithClientType("entity")). NewClientsSeen(4, clientcountutil.WithClientType("non-entity-token")). NewClientsSeen(2, clientcountutil.WithClientType("secret-sync")). NewClientsSeen(7, clientcountutil.WithClientType("pki-acme")). - NewCurrentMonthData(). + NewPreviousMonthData(1). NewClientsSeen(3, clientcountutil.WithClientType("entity")). NewClientsSeen(4, clientcountutil.WithClientType("non-entity-token")). NewClientsSeen(5, clientcountutil.WithClientType("secret-sync")). @@ -70,8 +70,8 @@ func TestOperatorUsageCommandRun(t *testing.T) { ui, cmd := testOperatorUsageCommand(t) t.Setenv("VAULT_TOKEN", client.Token()) - start := timeutil.MonthsPreviousTo(1, now).Format(time.RFC3339) - end := timeutil.EndOfMonth(now).UTC().Format(time.RFC3339) + start := timeutil.MonthsPreviousTo(2, now).Format(time.RFC3339) + end := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now)).UTC().Format(time.RFC3339) // Reset and check output code := cmd.Run([]string{ "-address", client.Address(), diff --git a/vault/activity_log.go b/vault/activity_log.go index 39875c7f02..2a65dba67b 100644 --- a/vault/activity_log.go +++ b/vault/activity_log.go @@ -2033,18 +2033,14 @@ func (a *ActivityLog) handleQuery(ctx context.Context, startTime, endTime time.T // Now populate the response based on breakdowns. responseData := make(map[string]interface{}) - responseData["start_time"] = pq.StartTime.Format(time.RFC3339) + responseData["start_time"] = startTime.Format(time.RFC3339) // If we computed partial counts, we should return the actual end time we computed counts for, not the pre-computed // query end time. If we don't do this, the end_time in the response doesn't match the actual data in the response, // which is confusing. Note that regardless of what end time is given, if it falls within the current month, it will // be set to the end of the current month. This is definitely suboptimal, and possibly confusing, but still an // improvement over using the pre-computed query end time. - if computePartial { - responseData["end_time"] = endTime.Format(time.RFC3339) - } else { - responseData["end_time"] = pq.EndTime.Format(time.RFC3339) - } + responseData["end_time"] = endTime.Format(time.RFC3339) responseData["by_namespace"] = byNamespaceResponse responseData["total"] = totalCounts diff --git a/vault/activity_log_testonly_oss_test.go b/vault/activity_log_testonly_oss_test.go index 9e18a16956..4554f69a74 100644 --- a/vault/activity_log_testonly_oss_test.go +++ b/vault/activity_log_testonly_oss_test.go @@ -12,6 +12,8 @@ import ( "time" "github.com/hashicorp/vault/helper/testhelpers/corehelpers" + "github.com/hashicorp/vault/helper/timeutil" + "github.com/hashicorp/vault/sdk/framework" "github.com/stretchr/testify/require" ) @@ -36,3 +38,99 @@ func TestActivityLog_setupClientIDsUsageInfo_CE(t *testing.T) { require.Len(t, a.GetClientIDsUsageInfo(), 0) } + +// TestGetStartEndTime_EndTimeAdjustedToPastMonth tests getStartEndTime for proper adjustment of given end time to past month +func TestGetStartEndTime_EndTimeAdjustedToPastMonth(t *testing.T) { + now := time.Now().UTC() + currentMonthStart := timeutil.StartOfMonth(now) + previousMonthEnd := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now)) + + // billing start time is zero for CE + billingStartTime := time.Time{} + sixMonthsAgo := now.AddDate(0, -6, 0) + + tests := []struct { + name string + givenStartTime time.Time + givenEndTime time.Time + expectedStart time.Time + expectedEnd time.Time + expectWarning bool + expectErr bool + }{ + { + name: "End time in the past is unchanged", + givenStartTime: sixMonthsAgo, + givenEndTime: now.AddDate(0, -1, -1), + expectedStart: sixMonthsAgo, + expectedEnd: now.AddDate(0, -1, -1).UTC(), + expectWarning: false, + expectErr: false, + }, + { + name: "End time in the current month is clamped to previous month", + givenStartTime: sixMonthsAgo, + givenEndTime: currentMonthStart.AddDate(0, 0, 5).Add(2 * time.Hour), + expectedStart: sixMonthsAgo, + expectedEnd: previousMonthEnd, + expectWarning: true, + expectErr: false, + }, + { + name: "End time in the future is clamped to previous month", + givenStartTime: sixMonthsAgo, + givenEndTime: now.AddDate(0, 1, 0), + expectedStart: sixMonthsAgo, + expectedEnd: previousMonthEnd, + expectWarning: true, + expectErr: false, + }, + { + name: "End time is zero and gets clamped to previous month", + givenStartTime: sixMonthsAgo, + givenEndTime: time.Time{}, + expectedStart: sixMonthsAgo, + expectedEnd: previousMonthEnd, + expectWarning: true, + expectErr: false, + }, + { + name: "Start time after end time causes error", + givenStartTime: now.AddDate(0, 2, 0), + givenEndTime: now.AddDate(0, 1, 0), + expectWarning: false, + expectErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + d := &framework.FieldData{ + Schema: map[string]*framework.FieldSchema{ + "start_time": { + Type: framework.TypeTime, + }, + "end_time": { + Type: framework.TypeTime, + }, + }, + Raw: map[string]any{ + "start_time": tc.givenStartTime.Format(time.RFC3339Nano), + "end_time": tc.givenEndTime.Format(time.RFC3339Nano), + }, + } + + start, end, warnings, err := getStartEndTime(d, billingStartTime) + + if tc.expectErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.WithinDuration(t, tc.expectedStart, start, time.Second, "Expected start time did not match") + require.WithinDuration(t, tc.expectedEnd, end, time.Second, "Expected end time did not match") + require.Equal(t, tc.expectWarning, warnings.EndTimeAdjustedToPastMonth) + }) + } +} diff --git a/vault/activity_log_util.go b/vault/activity_log_util.go index 0043c7da4a..9fda8006e6 100644 --- a/vault/activity_log_util.go +++ b/vault/activity_log_util.go @@ -8,6 +8,9 @@ package vault import ( "context" "time" + + "github.com/hashicorp/vault/helper/timeutil" + "github.com/hashicorp/vault/sdk/framework" ) // sendCurrentFragment is a no-op on OSS @@ -22,3 +25,22 @@ func (c *Core) setupClientIDsUsageInfo(ctx context.Context) { // handleClientIDsInMemoryEndOfMonth is a no-op on OSS func (a *ActivityLog) handleClientIDsInMemoryEndOfMonth(ctx context.Context, currentTime time.Time) { } + +// getStartEndTime parses input for start and end times +// If the end time is after the end of last month, it is adjusted to the last month +func getStartEndTime(d *framework.FieldData, billingStartTime time.Time) (time.Time, time.Time, StartEndTimesWarnings, error) { + warnings := StartEndTimesWarnings{} + startTime, endTime, err := parseStartEndTimes(d, billingStartTime) + if err != nil { + return startTime, endTime, warnings, err + } + // ensure end time is adjusted to the past month if it falls within the current month + // or is in a future month + now := time.Now().UTC() + if !endTime.Before(timeutil.StartOfMonth(now)) { + endTime = timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, timeutil.StartOfMonth(now))) + warnings.EndTimeAdjustedToPastMonth = true + } + + return startTime, endTime, warnings, nil +} diff --git a/vault/external_tests/activity_testonly/acme_regeneration_oss_test.go b/vault/external_tests/activity_testonly/acme_regeneration_oss_test.go new file mode 100644 index 0000000000..84b2e5291a --- /dev/null +++ b/vault/external_tests/activity_testonly/acme_regeneration_oss_test.go @@ -0,0 +1,95 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +//go:build testonly && !enterprise + +package activity_testonly + +import ( + "context" + "testing" + "time" + + "github.com/hashicorp/vault/helper/testhelpers/minimal" + "github.com/hashicorp/vault/helper/timeutil" + "github.com/hashicorp/vault/sdk/helper/clientcountutil" + "github.com/hashicorp/vault/sdk/helper/clientcountutil/generation" + "github.com/hashicorp/vault/vault" + "github.com/stretchr/testify/require" +) + +// TestACMERegeneration_RegenerateWithCurrentMonth writes segments for previous +// months and the current month. The test regenerates the precomputed queries, +// and verifies that the counts are correct when querying both with and without +// the current month +func TestACMERegeneration_RegenerateWithCurrentMonth(t *testing.T) { + t.Parallel() + cluster := minimal.NewTestSoloCluster(t, &vault.CoreConfig{EnableRaw: true}) + client := cluster.Cores[0].Client + _, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ + "enabled": "enable", + }) + require.NoError(t, err) + now := time.Now().UTC() + _, err = clientcountutil.NewActivityLogData(client). + NewPreviousMonthData(3). + // 3 months ago, 15 non-entity clients and 10 ACME clients + NewClientsSeen(15, clientcountutil.WithClientType("non-entity-token")). + NewClientsSeen(10, clientcountutil.WithClientType(vault.ACMEActivityType)). + NewPreviousMonthData(2). + // 2 months ago, 7 new non-entity clients and 5 new ACME clients + RepeatedClientsSeen(2, clientcountutil.WithClientType("non-entity-token")). + NewClientsSeen(7, clientcountutil.WithClientType("non-entity-token")). + RepeatedClientsSeen(5, clientcountutil.WithClientType(vault.ACMEActivityType)). + NewClientsSeen(5, clientcountutil.WithClientType(vault.ACMEActivityType)). + NewPreviousMonthData(1). + // 1 months ago, 4 new non-entity clients and 2 new ACME clients + RepeatedClientsSeen(3, clientcountutil.WithClientType("non-entity-token")). + NewClientsSeen(4, clientcountutil.WithClientType("non-entity-token")). + RepeatedClientsSeen(1, clientcountutil.WithClientType(vault.ACMEActivityType)). + NewClientsSeen(2, clientcountutil.WithClientType(vault.ACMEActivityType)). + + // current month, 10 new non-entity clients and 20 new ACME clients + NewCurrentMonthData(). + NewClientsSeen(10, clientcountutil.WithClientType("non-entity-token")). + NewClientsSeen(20, clientcountutil.WithClientType(vault.ACMEActivityType)). + Write(context.Background(), generation.WriteOptions_WRITE_ENTITIES) + + require.NoError(t, err) + + forceRegeneration(t, cluster) + + startFiveMonthsAgo := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(5, now)) + endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now)) + // current month isn't included in this query + resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ + "start_time": {startFiveMonthsAgo.Format(time.RFC3339)}, + "end_time": {endPastMonth.Format(time.RFC3339)}, + }) + require.NoError(t, err) + require.Equal(t, vault.ResponseCounts{ + NonEntityClients: 26, + Clients: 43, + ACMEClients: 17, + }, getTotals(t, resp)) + // verify start and end times in the response + require.Equal(t, resp.Data["start_time"], startFiveMonthsAgo.UTC().Format(time.RFC3339)) + require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339)) + + // explicitly include the current month in the request + // the given end time is adjusted to the last month, excluding the current month at the API + respWithCurrent, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ + "start_time": {startFiveMonthsAgo.Format(time.RFC3339)}, + "end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)}, + }) + // verify start and end times in the response + // end time is expected to be adjusted to the past month, excluding the current month + require.Equal(t, resp.Data["start_time"], startFiveMonthsAgo.UTC().Format(time.RFC3339)) + require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339)) + require.NoError(t, err) + require.Equal(t, vault.ResponseCounts{ + NonEntityClients: 26, + Clients: 43, + ACMEClients: 17, + }, getTotals(t, respWithCurrent)) +} diff --git a/vault/external_tests/activity_testonly/acme_regeneration_test.go b/vault/external_tests/activity_testonly/acme_regeneration_test.go index dbd8355f81..8d0e1de339 100644 --- a/vault/external_tests/activity_testonly/acme_regeneration_test.go +++ b/vault/external_tests/activity_testonly/acme_regeneration_test.go @@ -41,72 +41,6 @@ func forceRegeneration(t *testing.T, cluster *vault.TestCluster) { }) } -// TestACMERegeneration_RegenerateWithCurrentMonth writes segments for previous -// months and the current month. The test regenerates the precomputed queries, -// and verifies that the counts are correct when querying both with and without -// the current month -func TestACMERegeneration_RegenerateWithCurrentMonth(t *testing.T) { - t.Parallel() - cluster := minimal.NewTestSoloCluster(t, &vault.CoreConfig{EnableRaw: true}) - client := cluster.Cores[0].Client - _, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ - "enabled": "enable", - }) - require.NoError(t, err) - now := time.Now().UTC() - _, err = clientcountutil.NewActivityLogData(client). - NewPreviousMonthData(3). - // 3 months ago, 15 non-entity clients and 10 ACME clients - NewClientsSeen(15, clientcountutil.WithClientType("non-entity-token")). - NewClientsSeen(10, clientcountutil.WithClientType(vault.ACMEActivityType)). - NewPreviousMonthData(2). - // 2 months ago, 7 new non-entity clients and 5 new ACME clients - RepeatedClientsSeen(2, clientcountutil.WithClientType("non-entity-token")). - NewClientsSeen(7, clientcountutil.WithClientType("non-entity-token")). - RepeatedClientsSeen(5, clientcountutil.WithClientType(vault.ACMEActivityType)). - NewClientsSeen(5, clientcountutil.WithClientType(vault.ACMEActivityType)). - NewPreviousMonthData(1). - // 1 months ago, 4 new non-entity clients and 2 new ACME clients - RepeatedClientsSeen(3, clientcountutil.WithClientType("non-entity-token")). - NewClientsSeen(4, clientcountutil.WithClientType("non-entity-token")). - RepeatedClientsSeen(1, clientcountutil.WithClientType(vault.ACMEActivityType)). - NewClientsSeen(2, clientcountutil.WithClientType(vault.ACMEActivityType)). - - // current month, 10 new non-entity clients and 20 new ACME clients - NewCurrentMonthData(). - NewClientsSeen(10, clientcountutil.WithClientType("non-entity-token")). - NewClientsSeen(20, clientcountutil.WithClientType(vault.ACMEActivityType)). - Write(context.Background(), generation.WriteOptions_WRITE_ENTITIES) - - require.NoError(t, err) - - forceRegeneration(t, cluster) - - // current month isn't included in this query - resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ - "start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(5, now)).Format(time.RFC3339)}, - "end_time": {timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now)).Format(time.RFC3339)}, - }) - require.NoError(t, err) - require.Equal(t, vault.ResponseCounts{ - NonEntityClients: 26, - Clients: 43, - ACMEClients: 17, - }, getTotals(t, resp)) - - // explicitly include the current month - respWithCurrent, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ - "start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(5, now)).Format(time.RFC3339)}, - "end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)}, - }) - require.NoError(t, err) - require.Equal(t, vault.ResponseCounts{ - NonEntityClients: 36, - Clients: 73, - ACMEClients: 37, - }, getTotals(t, respWithCurrent)) -} - // TestACMERegeneration_RegenerateMuchOlder creates segments 5 months ago, 4 // months ago, and 3 months ago. The test regenerates the precomputed queries // and then verifies that this older data is included in the generated results. diff --git a/vault/external_tests/activity_testonly/activity_testonly_oss_test.go b/vault/external_tests/activity_testonly/activity_testonly_oss_test.go index dbb11c8453..245047f81b 100644 --- a/vault/external_tests/activity_testonly/activity_testonly_oss_test.go +++ b/vault/external_tests/activity_testonly/activity_testonly_oss_test.go @@ -7,13 +7,21 @@ package activity_testonly import ( "context" + "fmt" + "math" "testing" "time" + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/helper/namespace" + "github.com/hashicorp/vault/helper/testhelpers" "github.com/hashicorp/vault/helper/testhelpers/minimal" "github.com/hashicorp/vault/helper/timeutil" + vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/helper/clientcountutil" "github.com/hashicorp/vault/sdk/helper/clientcountutil/generation" + "github.com/hashicorp/vault/vault" + "github.com/mitchellh/mapstructure" "github.com/stretchr/testify/require" ) @@ -59,3 +67,516 @@ func Test_ActivityLog_Disable(t *testing.T) { require.NoError(t, err) require.Equal(t, ts.UTC(), timeutil.StartOfPreviousMonth(now.UTC())) } + +// Test_ActivityLog_LoseLeadership writes data for the second last month, then causes the +// active node to lose leadership. Once a new node becomes the leader, then the +// test queries for the second last month data and verifies that the data from +// before the leadership transfer is returned +func Test_ActivityLog_LoseLeadership(t *testing.T) { + t.Parallel() + cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + NumCores: 2, + }) + cluster.Start() + defer cluster.Cleanup() + + active := testhelpers.DeriveStableActiveCore(t, cluster) + client := active.Client + _, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ + "enabled": "enable", + }) + require.NoError(t, err) + + _, err = clientcountutil.NewActivityLogData(client). + NewPreviousMonthData(1). + NewClientsSeen(10). + Write(context.Background(), generation.WriteOptions_WRITE_ENTITIES, generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES) + require.NoError(t, err) + now := time.Now().UTC() + + testhelpers.EnsureCoreSealed(t, active) + newActive := testhelpers.WaitForActiveNode(t, cluster) + standby := active + testhelpers.WaitForStandbyNode(t, standby) + testhelpers.EnsureCoreUnsealed(t, cluster, standby) + + endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now)) + startPastMonth := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(1, now)) + resp, err := newActive.Client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ + "end_time": {endPastMonth.Format(time.RFC3339)}, + "start_time": {startPastMonth.Format(time.RFC3339)}, + }) + require.NoError(t, err) + // verify start and end times in the response + require.Equal(t, resp.Data["start_time"], startPastMonth.UTC().Format(time.RFC3339)) + require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339)) + monthResponse := getMonthsData(t, resp) + require.Len(t, monthResponse, 1) + require.Equal(t, 10, monthResponse[0].NewClients.Counts.Clients) +} + +// Test_ActivityLog_ClientsOverlapping writes data for the second last month and +// the previous month. In the second last month, 7 new clients are seen. In the previous +// month, there are 5 repeated and 2 new clients. The test queries over the +// second last and previous months, and verifies that the repeated clients are not +// considered new +func Test_ActivityLog_ClientsOverlapping(t *testing.T) { + t.Parallel() + cluster := minimal.NewTestSoloCluster(t, nil) + client := cluster.Cores[0].Client + _, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ + "enabled": "enable", + }) + require.NoError(t, err) + _, err = clientcountutil.NewActivityLogData(client). + NewPreviousMonthData(2). + NewClientsSeen(7). + NewPreviousMonthData(1). + RepeatedClientsSeen(5). + NewClientsSeen(2). + Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES) + require.NoError(t, err) + + now := time.Now().UTC() + endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now)) + startTwoMonthsAgo := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(2, now)) + // query from the beginning of the second last month to the end of the previous month + resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ + "end_time": {endPastMonth.Format(time.RFC3339)}, + "start_time": {startTwoMonthsAgo.Format(time.RFC3339)}, + }) + require.NoError(t, err) + // verify start and end times in the response + require.Equal(t, resp.Data["start_time"], startTwoMonthsAgo.UTC().Format(time.RFC3339)) + require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339)) + monthsResponse := getMonthsData(t, resp) + require.Len(t, monthsResponse, 2) + for _, month := range monthsResponse { + ts, err := time.Parse(time.RFC3339, month.Timestamp) + require.NoError(t, err) + // The previous month should have a total of 7 clients + // 2 of those will be considered new + if ts.UTC().Equal(timeutil.StartOfMonth(timeutil.MonthsPreviousTo(1, now))) { + require.Equal(t, month.Counts.Clients, 7) + require.Equal(t, month.NewClients.Counts.Clients, 2) + } else { + // All clients will be considered new for the second last month + require.Equal(t, month.Counts.Clients, 7) + require.Equal(t, month.NewClients.Counts.Clients, 7) + + } + } +} + +// Test_ActivityLog_ClientsNewCurrentMonth writes data for the second last month and +// past month with 5 repeated clients and 2 new clients in the past month. +// The test then queries the activity log for only the past month, and +// verifies that all 7 clients seen the past month are considered new. +func Test_ActivityLog_ClientsNewCurrentMonth(t *testing.T) { + t.Parallel() + cluster := minimal.NewTestSoloCluster(t, nil) + client := cluster.Cores[0].Client + _, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ + "enabled": "enable", + }) + require.NoError(t, err) + _, err = clientcountutil.NewActivityLogData(client). + NewPreviousMonthData(2). + NewClientsSeen(5). + NewPreviousMonthData(1). + RepeatedClientsSeen(5). + NewClientsSeen(2). + Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES) + require.NoError(t, err) + + now := time.Now().UTC() + endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now)) + startPastMonth := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(1, now)) + // query from the beginning of the second last month to the end of the previous month + resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ + "end_time": {endPastMonth.Format(time.RFC3339)}, + "start_time": {startPastMonth.Format(time.RFC3339)}, + }) + // verify start and end times in the response + require.Equal(t, resp.Data["start_time"], startPastMonth.UTC().Format(time.RFC3339)) + require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339)) + require.NoError(t, err) + monthsResponse := getMonthsData(t, resp) + require.Len(t, monthsResponse, 1) + require.Equal(t, 7, monthsResponse[0].NewClients.Counts.Clients) +} + +// Test_ActivityLog_EmptyDataMonths writes data for only the past month, +// then queries a timeframe of several months in the past to now. The test +// verifies that empty months of data are returned for the past, and the past +// month data is correct. +func Test_ActivityLog_EmptyDataMonths(t *testing.T) { + t.Parallel() + cluster := minimal.NewTestSoloCluster(t, nil) + client := cluster.Cores[0].Client + _, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ + "enabled": "enable", + }) + require.NoError(t, err) + _, err = clientcountutil.NewActivityLogData(client). + NewPreviousMonthData(1). + NewClientsSeen(10). + Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES) + require.NoError(t, err) + + now := time.Now().UTC() + endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now)) + startFourMonthsAgo := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(4, now)) + // query from the beginning of 4 months ago to the end of past month + resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ + "end_time": {endPastMonth.Format(time.RFC3339)}, + "start_time": {startFourMonthsAgo.Format(time.RFC3339)}, + }) + require.NoError(t, err) + // verify start and end times in the response + require.Equal(t, resp.Data["start_time"], startFourMonthsAgo.UTC().Format(time.RFC3339)) + require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339)) + monthsResponse := getMonthsData(t, resp) + + require.Len(t, monthsResponse, 4) + for _, month := range monthsResponse { + ts, err := time.Parse(time.RFC3339, month.Timestamp) + require.NoError(t, err) + // past month should have data + if ts.UTC().Equal(timeutil.StartOfMonth(timeutil.MonthsPreviousTo(1, now))) { + require.Equal(t, month.Counts.Clients, 10) + } else { + // other months should be empty + require.Nil(t, month.Counts) + } + } +} + +// Test_ActivityLog_FutureEndDate queries a start time from the past +// and an end date in the future. The test +// verifies that the current month is returned in the response. +func Test_ActivityLog_FutureEndDate(t *testing.T) { + t.Parallel() + cluster := minimal.NewTestSoloCluster(t, nil) + client := cluster.Cores[0].Client + _, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ + "enabled": "enable", + }) + require.NoError(t, err) + _, err = clientcountutil.NewActivityLogData(client). + NewPreviousMonthData(1). + NewClientsSeen(10). + NewCurrentMonthData(). + NewClientsSeen(10). + Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES) + require.NoError(t, err) + + now := time.Now().UTC() + startThreeMonthsAgo := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(3, now)) + // query from the beginning of 3 months ago to beginning of next month + resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ + "end_time": {timeutil.StartOfNextMonth(now).Format(time.RFC3339)}, + "start_time": {startThreeMonthsAgo.Format(time.RFC3339)}, + }) + require.NoError(t, err) + endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now)) + // verify start and end times in the response + // end time must be adjusted to past month if within current month or in future date + require.Equal(t, resp.Data["start_time"], startThreeMonthsAgo.UTC().Format(time.RFC3339)) + require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339)) + monthsResponse := getMonthsData(t, resp) + + require.Len(t, monthsResponse, 3) + + // Get the last month of data in the slice + expectedCurrentMonthData := monthsResponse[2] + expectedTime, err := time.Parse(time.RFC3339, expectedCurrentMonthData.Timestamp) + require.NoError(t, err) + if !timeutil.IsCurrentMonth(expectedTime, endPastMonth) { + t.Fatalf("final month data is not past month") + } +} + +// Test_ActivityLog_ClientTypeResponse runs for each client type. In the +// subtests, 10 clients of the type are created and the test verifies that the +// activity log query response returns 10 clients of that type at every level of +// the response hierarchy +func Test_ActivityLog_ClientTypeResponse(t *testing.T) { + t.Parallel() + for _, tc := range allClientTypeTestCases { + tc := tc + t.Run(tc.clientType, func(t *testing.T) { + t.Parallel() + cluster := minimal.NewTestSoloCluster(t, nil) + client := cluster.Cores[0].Client + _, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ + "enabled": "enable", + }) + _, err = clientcountutil.NewActivityLogData(client). + NewPreviousMonthData(1). + NewClientsSeen(10, clientcountutil.WithClientType(tc.clientType)). + Write(context.Background(), generation.WriteOptions_WRITE_ENTITIES, generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES) + require.NoError(t, err) + + now := time.Now().UTC() + endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now)) + startPastMonth := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(1, now)) + resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ + "end_time": {endPastMonth.Format(time.RFC3339)}, + "start_time": {startPastMonth.Format(time.RFC3339)}, + }) + require.NoError(t, err) + // verify start and end times in the response + require.Equal(t, resp.Data["start_time"], startPastMonth.UTC().Format(time.RFC3339)) + require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339)) + + total := getTotals(t, resp) + require.Equal(t, 10, tc.responseCountsFn(total)) + require.Equal(t, 10, total.Clients) + + byNamespace := getNamespaceData(t, resp) + require.Equal(t, 10, tc.responseCountsFn(byNamespace[0].Counts)) + require.Equal(t, 10, tc.responseCountsFn(*byNamespace[0].Mounts[0].Counts)) + require.Equal(t, 10, byNamespace[0].Counts.Clients) + require.Equal(t, 10, byNamespace[0].Mounts[0].Counts.Clients) + + byMonth := getMonthsData(t, resp) + require.Equal(t, 10, tc.responseCountsFn(*byMonth[0].NewClients.Counts)) + require.Equal(t, 10, tc.responseCountsFn(*byMonth[0].Counts)) + require.Equal(t, 10, tc.responseCountsFn(byMonth[0].Namespaces[0].Counts)) + require.Equal(t, 10, tc.responseCountsFn(*byMonth[0].Namespaces[0].Mounts[0].Counts)) + require.Equal(t, 10, byMonth[0].NewClients.Counts.Clients) + require.Equal(t, 10, byMonth[0].Counts.Clients) + require.Equal(t, 10, byMonth[0].Namespaces[0].Counts.Clients) + require.Equal(t, 10, byMonth[0].Namespaces[0].Mounts[0].Counts.Clients) + }) + + } +} + +// Test_ActivityLog_MountDeduplication writes data for the second last +// month across 4 mounts. The cubbyhole and sys mounts have clients in the +// past month as well. The test verifies that the mount counts are correctly +// summed in the results when the second last month and past month are queried. +func Test_ActivityLog_MountDeduplication(t *testing.T) { + t.Parallel() + + cluster := minimal.NewTestSoloCluster(t, nil) + client := cluster.Cores[0].Client + _, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ + "enabled": "enable", + }) + require.NoError(t, err) + now := time.Now().UTC() + + _, err = clientcountutil.NewActivityLogData(client). + NewPreviousMonthData(2). + NewClientSeen(clientcountutil.WithClientMount("sys")). + NewClientSeen(clientcountutil.WithClientMount("secret")). + NewClientSeen(clientcountutil.WithClientMount("cubbyhole")). + NewClientSeen(clientcountutil.WithClientMount("identity")). + NewPreviousMonthData(1). + NewClientSeen(clientcountutil.WithClientMount("cubbyhole")). + NewClientSeen(clientcountutil.WithClientMount("sys")). + Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES) + require.NoError(t, err) + + endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, now)) + startTwoMonthsAgo := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(2, now)) + resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ + "end_time": {endPastMonth.Format(time.RFC3339)}, + "start_time": {startTwoMonthsAgo.Format(time.RFC3339)}, + }) + // verify start and end times in the response + require.Equal(t, resp.Data["start_time"], startTwoMonthsAgo.UTC().Format(time.RFC3339)) + require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339)) + + require.NoError(t, err) + byNamespace := getNamespaceData(t, resp) + require.Len(t, byNamespace, 1) + require.Len(t, byNamespace[0].Mounts, 4) + mountSet := make(map[string]int, 4) + for _, mount := range byNamespace[0].Mounts { + mountSet[mount.MountPath] = mount.Counts.Clients + } + require.Equal(t, map[string]int{ + "identity/": 1, + "sys/": 2, + "cubbyhole/": 2, + "secret/": 1, + }, mountSet) +} + +// TestHandleQuery_MultipleMounts creates a cluster with +// two userpass mounts. It then tests verifies that +// the total new counts are calculated within a reasonably level of accuracy for +// various numbers of clients in each mount. +func TestHandleQuery_MultipleMounts(t *testing.T) { + tests := map[string]struct { + twoMonthsAgo [][]int + oneMonthAgo [][]int + currentMonth [][]int + expectedNewClients int + expectedTotalAccuracy float64 + }{ + "low volume, all mounts": { + twoMonthsAgo: [][]int{ + {20, 20}, + }, + oneMonthAgo: [][]int{ + {30, 30}, + }, + currentMonth: [][]int{ + {40, 40}, + }, + expectedNewClients: 80, + expectedTotalAccuracy: 1, + }, + "medium volume, all mounts": { + twoMonthsAgo: [][]int{ + {200, 200}, + }, + oneMonthAgo: [][]int{ + {300, 300}, + }, + currentMonth: [][]int{ + {400, 400}, + }, + expectedNewClients: 800, + expectedTotalAccuracy: 0.98, + }, + "higher volume, all mounts": { + twoMonthsAgo: [][]int{ + {200, 200}, + }, + oneMonthAgo: [][]int{ + {300, 300}, + }, + currentMonth: [][]int{ + {2000, 5000}, + }, + expectedNewClients: 7000, + expectedTotalAccuracy: 0.95, + }, + "higher volume, no repeats": { + twoMonthsAgo: [][]int{ + {200, 200}, + }, + oneMonthAgo: [][]int{ + {300, 300}, + }, + currentMonth: [][]int{ + {4000, 6000}, + }, + expectedNewClients: 10000, + expectedTotalAccuracy: 0.98, + }, + } + + for i, tt := range tests { + testname := fmt.Sprintf("%s", i) + t.Run(testname, func(t *testing.T) { + var err error + cluster := minimal.NewTestSoloCluster(t, nil) + client := cluster.Cores[0].Client + _, err = client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ + "enabled": "enable", + }) + require.NoError(t, err) + + // Create two namespaces + namespaces := []string{namespace.RootNamespaceID} + mounts := make(map[string][]string) + + // Add two userpass mounts to each namespace + for _, ns := range namespaces { + err = client.WithNamespace(ns).Sys().EnableAuthWithOptions("userpass1", &api.EnableAuthOptions{ + Type: "userpass", + }) + require.NoError(t, err) + err = client.WithNamespace(ns).Sys().EnableAuthWithOptions("userpass2", &api.EnableAuthOptions{ + Type: "userpass", + }) + require.NoError(t, err) + mounts[ns] = []string{"auth/userpass1", "auth/userpass2"} + } + + activityLogGenerator := clientcountutil.NewActivityLogData(client) + + // Write three months ago data + activityLogGenerator = activityLogGenerator.NewPreviousMonthData(3) + for nsIndex, nsId := range namespaces { + for mountIndex, mount := range mounts[nsId] { + activityLogGenerator = activityLogGenerator. + NewClientsSeen(tt.twoMonthsAgo[nsIndex][mountIndex], clientcountutil.WithClientNamespace(nsId), clientcountutil.WithClientMount(mount)) + } + } + + // Write two months ago data + activityLogGenerator = activityLogGenerator.NewPreviousMonthData(2) + for nsIndex, nsId := range namespaces { + for mountIndex, mount := range mounts[nsId] { + activityLogGenerator = activityLogGenerator. + NewClientsSeen(tt.oneMonthAgo[nsIndex][mountIndex], clientcountutil.WithClientNamespace(nsId), clientcountutil.WithClientMount(mount)) + } + } + + // Write previous month data + activityLogGenerator = activityLogGenerator.NewPreviousMonthData(1) + for nsIndex, nsPath := range namespaces { + for mountIndex, mount := range mounts[nsPath] { + activityLogGenerator = activityLogGenerator. + RepeatedClientSeen(clientcountutil.WithClientNamespace(nsPath), clientcountutil.WithClientMount(mount)). + NewClientsSeen(tt.currentMonth[nsIndex][mountIndex], clientcountutil.WithClientNamespace(nsPath), clientcountutil.WithClientMount(mount)) + } + } + + // Write all the client count data + _, err = activityLogGenerator.Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES) + require.NoError(t, err) + + endPastMonth := timeutil.EndOfMonth(timeutil.MonthsPreviousTo(1, time.Now()).UTC()) + startThreeMonthsAgo := timeutil.StartOfMonth(timeutil.MonthsPreviousTo(3, time.Now().UTC())) + + // query activity log + resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ + "end_time": {endPastMonth.Format(time.RFC3339)}, + "start_time": {startThreeMonthsAgo.Format(time.RFC3339)}, + }) + require.NoError(t, err) + // verify start and end times in the response + require.Equal(t, resp.Data["start_time"], startThreeMonthsAgo.UTC().Format(time.RFC3339)) + require.Equal(t, resp.Data["end_time"], endPastMonth.UTC().Format(time.RFC3339)) + + // Ensure that the month response is the same as the totals, because all clients + // are new clients and there will be no approximation in the single month partial + // case + monthsRaw, ok := resp.Data["months"] + if !ok { + t.Fatalf("malformed results. got %v", resp.Data) + } + monthsResponse := make([]*vault.ResponseMonth, 0) + err = mapstructure.Decode(monthsRaw, &monthsResponse) + + currentMonthClients := monthsResponse[len(monthsResponse)-1] + + // Now verify that the new client totals for ALL namespaces are approximately accurate (there are no namespaces in CE) + newClientsError := math.Abs((float64)(currentMonthClients.NewClients.Counts.Clients - tt.expectedNewClients)) + newClientsErrorMargin := newClientsError / (float64)(tt.expectedNewClients) + expectedAccuracyCalc := (1 - tt.expectedTotalAccuracy) * 100 / 100 + if newClientsErrorMargin > expectedAccuracyCalc { + t.Fatalf("bad accuracy: expected %+v, found %+v", expectedAccuracyCalc, newClientsErrorMargin) + } + + // Verify that the totals for the clients are visibly sensible (that is the total of all the individual new clients per namespace) + total := 0 + for _, newClientCounts := range currentMonthClients.NewClients.Namespaces { + total += newClientCounts.Counts.Clients + } + if diff := math.Abs(float64(currentMonthClients.NewClients.Counts.Clients - total)); diff >= 1 { + t.Fatalf("total expected was %d but got %d", currentMonthClients.NewClients.Counts.Clients, total) + } + }) + } +} diff --git a/vault/external_tests/activity_testonly/activity_testonly_test.go b/vault/external_tests/activity_testonly/activity_testonly_test.go index 644ba70e8d..44a66d8a03 100644 --- a/vault/external_tests/activity_testonly/activity_testonly_test.go +++ b/vault/external_tests/activity_testonly/activity_testonly_test.go @@ -10,20 +10,15 @@ import ( "context" "encoding/csv" "encoding/json" - "fmt" "io" - "math" "strconv" "strings" "testing" "time" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/helper/testhelpers" "github.com/hashicorp/vault/helper/testhelpers/minimal" "github.com/hashicorp/vault/helper/timeutil" - vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/helper/clientcountutil" "github.com/hashicorp/vault/sdk/helper/clientcountutil/generation" "github.com/hashicorp/vault/vault" @@ -66,211 +61,6 @@ var allClientTypeTestCases = []struct { }, } -// Test_ActivityLog_LoseLeadership writes data for this month, then causes the -// active node to lose leadership. Once a new node becomes the leader, then the -// test queries for the current month data and verifies that the data from -// before the leadership transfer is returned -func Test_ActivityLog_LoseLeadership(t *testing.T) { - t.Parallel() - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - NumCores: 2, - }) - cluster.Start() - defer cluster.Cleanup() - - active := testhelpers.DeriveStableActiveCore(t, cluster) - client := active.Client - _, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ - "enabled": "enable", - }) - require.NoError(t, err) - - _, err = clientcountutil.NewActivityLogData(client). - NewCurrentMonthData(). - NewClientsSeen(10). - Write(context.Background(), generation.WriteOptions_WRITE_ENTITIES) - require.NoError(t, err) - now := time.Now().UTC() - - testhelpers.EnsureCoreSealed(t, active) - newActive := testhelpers.WaitForActiveNode(t, cluster) - standby := active - testhelpers.WaitForStandbyNode(t, standby) - testhelpers.EnsureCoreUnsealed(t, cluster, standby) - - resp, err := newActive.Client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ - "end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)}, - "start_time": {timeutil.StartOfMonth(now).Format(time.RFC3339)}, - }) - monthResponse := getMonthsData(t, resp) - require.Len(t, monthResponse, 1) - require.Equal(t, 10, monthResponse[0].NewClients.Counts.Clients) -} - -// Test_ActivityLog_ClientsOverlapping writes data for the previous month and -// current month. In the previous month, 7 new clients are seen. In the current -// month, there are 5 repeated and 2 new clients. The test queries over the -// previous and current months, and verifies that the repeated clients are not -// considered new -func Test_ActivityLog_ClientsOverlapping(t *testing.T) { - t.Parallel() - cluster := minimal.NewTestSoloCluster(t, nil) - client := cluster.Cores[0].Client - _, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ - "enabled": "enable", - }) - require.NoError(t, err) - _, err = clientcountutil.NewActivityLogData(client). - NewPreviousMonthData(1). - NewClientsSeen(7). - NewCurrentMonthData(). - RepeatedClientsSeen(5). - NewClientsSeen(2). - Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES) - require.NoError(t, err) - - now := time.Now().UTC() - - // query from the beginning of the previous month to the end of this month - resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ - "end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)}, - "start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(1, now)).Format(time.RFC3339)}, - }) - require.NoError(t, err) - monthsResponse := getMonthsData(t, resp) - require.Len(t, monthsResponse, 2) - for _, month := range monthsResponse { - ts, err := time.Parse(time.RFC3339, month.Timestamp) - require.NoError(t, err) - // This month should have a total of 7 clients - // 2 of those will be considered new - if ts.UTC().Equal(timeutil.StartOfMonth(now)) { - require.Equal(t, month.Counts.Clients, 7) - require.Equal(t, month.NewClients.Counts.Clients, 2) - } else { - // All clients will be considered new for the previous month - require.Equal(t, month.Counts.Clients, 7) - require.Equal(t, month.NewClients.Counts.Clients, 7) - - } - } -} - -// Test_ActivityLog_ClientsNewCurrentMonth writes data for the past month and -// current month with 5 repeated clients and 2 new clients in the current month. -// The test then queries the activity log for only the current month, and -// verifies that all 7 clients seen this month are considered new. -func Test_ActivityLog_ClientsNewCurrentMonth(t *testing.T) { - t.Parallel() - cluster := minimal.NewTestSoloCluster(t, nil) - client := cluster.Cores[0].Client - _, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ - "enabled": "enable", - }) - require.NoError(t, err) - _, err = clientcountutil.NewActivityLogData(client). - NewPreviousMonthData(1). - NewClientsSeen(5). - NewCurrentMonthData(). - RepeatedClientsSeen(5). - NewClientsSeen(2). - Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES) - require.NoError(t, err) - - now := time.Now().UTC() - - // query from the beginning of this month to the end of this month - resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ - "end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)}, - "start_time": {timeutil.StartOfMonth(now).Format(time.RFC3339)}, - }) - require.NoError(t, err) - monthsResponse := getMonthsData(t, resp) - require.Len(t, monthsResponse, 1) - require.Equal(t, 7, monthsResponse[0].NewClients.Counts.Clients) -} - -// Test_ActivityLog_EmptyDataMonths writes data for only the current month, -// then queries a timeframe of several months in the past to now. The test -// verifies that empty months of data are returned for the past, and the current -// month data is correct. -func Test_ActivityLog_EmptyDataMonths(t *testing.T) { - t.Parallel() - cluster := minimal.NewTestSoloCluster(t, nil) - client := cluster.Cores[0].Client - _, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ - "enabled": "enable", - }) - require.NoError(t, err) - _, err = clientcountutil.NewActivityLogData(client). - NewCurrentMonthData(). - NewClientsSeen(10). - Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES) - require.NoError(t, err) - - now := time.Now().UTC() - // query from the beginning of 3 months ago to the end of this month - resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ - "end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)}, - "start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(3, now)).Format(time.RFC3339)}, - }) - require.NoError(t, err) - monthsResponse := getMonthsData(t, resp) - - require.Len(t, monthsResponse, 4) - for _, month := range monthsResponse { - ts, err := time.Parse(time.RFC3339, month.Timestamp) - require.NoError(t, err) - // current month should have data - if ts.UTC().Equal(timeutil.StartOfMonth(now)) { - require.Equal(t, month.Counts.Clients, 10) - } else { - // other months should be empty - require.Nil(t, month.Counts) - } - } -} - -// Test_ActivityLog_FutureEndDate queries a start time from the past -// and an end date in the future. The test -// verifies that the current month is returned in the response. -func Test_ActivityLog_FutureEndDate(t *testing.T) { - t.Parallel() - cluster := minimal.NewTestSoloCluster(t, nil) - client := cluster.Cores[0].Client - _, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ - "enabled": "enable", - }) - require.NoError(t, err) - _, err = clientcountutil.NewActivityLogData(client). - NewPreviousMonthData(1). - NewClientsSeen(10). - NewCurrentMonthData(). - NewClientsSeen(10). - Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES) - require.NoError(t, err) - - now := time.Now().UTC() - // query from the beginning of 3 months ago to beginning of next month - resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ - "end_time": {timeutil.StartOfNextMonth(now).Format(time.RFC3339)}, - "start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(3, now)).Format(time.RFC3339)}, - }) - require.NoError(t, err) - monthsResponse := getMonthsData(t, resp) - - require.Len(t, monthsResponse, 4) - - // Get the last month of data in the slice - expectedCurrentMonthData := monthsResponse[3] - expectedTime, err := time.Parse(time.RFC3339, expectedCurrentMonthData.Timestamp) - require.NoError(t, err) - if !timeutil.IsCurrentMonth(expectedTime, now) { - t.Fatalf("final month data is not current month") - } -} - func getMonthsData(t *testing.T, resp *api.Secret) []vault.ResponseMonth { t.Helper() monthsRaw, ok := resp.Data["months"] @@ -301,58 +91,6 @@ func getTotals(t *testing.T, resp *api.Secret) vault.ResponseCounts { return total } -// Test_ActivityLog_ClientTypeResponse runs for each client type. In the -// subtests, 10 clients of the type are created and the test verifies that the -// activity log query response returns 10 clients of that type at every level of -// the response hierarchy -func Test_ActivityLog_ClientTypeResponse(t *testing.T) { - t.Parallel() - for _, tc := range allClientTypeTestCases { - tc := tc - t.Run(tc.clientType, func(t *testing.T) { - t.Parallel() - cluster := minimal.NewTestSoloCluster(t, nil) - client := cluster.Cores[0].Client - _, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ - "enabled": "enable", - }) - _, err = clientcountutil.NewActivityLogData(client). - NewCurrentMonthData(). - NewClientsSeen(10, clientcountutil.WithClientType(tc.clientType)). - Write(context.Background(), generation.WriteOptions_WRITE_ENTITIES) - require.NoError(t, err) - - now := time.Now().UTC() - resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ - "end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)}, - "start_time": {timeutil.StartOfMonth(now).Format(time.RFC3339)}, - }) - require.NoError(t, err) - - total := getTotals(t, resp) - require.Equal(t, 10, tc.responseCountsFn(total)) - require.Equal(t, 10, total.Clients) - - byNamespace := getNamespaceData(t, resp) - require.Equal(t, 10, tc.responseCountsFn(byNamespace[0].Counts)) - require.Equal(t, 10, tc.responseCountsFn(*byNamespace[0].Mounts[0].Counts)) - require.Equal(t, 10, byNamespace[0].Counts.Clients) - require.Equal(t, 10, byNamespace[0].Mounts[0].Counts.Clients) - - byMonth := getMonthsData(t, resp) - require.Equal(t, 10, tc.responseCountsFn(*byMonth[0].NewClients.Counts)) - require.Equal(t, 10, tc.responseCountsFn(*byMonth[0].Counts)) - require.Equal(t, 10, tc.responseCountsFn(byMonth[0].Namespaces[0].Counts)) - require.Equal(t, 10, tc.responseCountsFn(*byMonth[0].Namespaces[0].Mounts[0].Counts)) - require.Equal(t, 10, byMonth[0].NewClients.Counts.Clients) - require.Equal(t, 10, byMonth[0].Counts.Clients) - require.Equal(t, 10, byMonth[0].Namespaces[0].Counts.Clients) - require.Equal(t, 10, byMonth[0].Namespaces[0].Mounts[0].Counts.Clients) - }) - - } -} - // Test_ActivityLogCurrentMonth_Response runs for each client type. The subtest // creates 10 clients of the type and verifies that the activity log partial // month response returns 10 clients of that type at every level of the response @@ -447,54 +185,6 @@ func Test_ActivityLog_Deduplication(t *testing.T) { } } -// Test_ActivityLog_MountDeduplication writes data for the previous -// month across 4 mounts. The cubbyhole and sys mounts have clients in the -// current month as well. The test verifies that the mount counts are correctly -// summed in the results when the previous and current month are queried. -func Test_ActivityLog_MountDeduplication(t *testing.T) { - t.Parallel() - - cluster := minimal.NewTestSoloCluster(t, nil) - client := cluster.Cores[0].Client - _, err := client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ - "enabled": "enable", - }) - require.NoError(t, err) - now := time.Now().UTC() - - _, err = clientcountutil.NewActivityLogData(client). - NewPreviousMonthData(1). - NewClientSeen(clientcountutil.WithClientMount("sys")). - NewClientSeen(clientcountutil.WithClientMount("secret")). - NewClientSeen(clientcountutil.WithClientMount("cubbyhole")). - NewClientSeen(clientcountutil.WithClientMount("identity")). - NewCurrentMonthData(). - NewClientSeen(clientcountutil.WithClientMount("cubbyhole")). - NewClientSeen(clientcountutil.WithClientMount("sys")). - Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES) - require.NoError(t, err) - - resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ - "end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)}, - "start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(1, now)).Format(time.RFC3339)}, - }) - - require.NoError(t, err) - byNamespace := getNamespaceData(t, resp) - require.Len(t, byNamespace, 1) - require.Len(t, byNamespace[0].Mounts, 4) - mountSet := make(map[string]int, 4) - for _, mount := range byNamespace[0].Mounts { - mountSet[mount.MountPath] = mount.Counts.Clients - } - require.Equal(t, map[string]int{ - "identity/": 1, - "sys/": 2, - "cubbyhole/": 2, - "secret/": 1, - }, mountSet) -} - // getJSONExport is used to fetch activity export records using json format. // The records will be returned as a map keyed by client ID. func getJSONExport(t *testing.T, client *api.Client, startTime time.Time, now time.Time) (map[string]vault.ActivityLogExportRecord, error) { @@ -720,172 +410,3 @@ path "sys/internal/counters/activity/export" { require.NoError(t, err) require.Len(t, clients, 10) } - -// TestHandleQuery_MultipleMounts creates a cluster with -// two userpass mounts. It then tests verifies that -// the total new counts are calculated within a reasonably level of accuracy for -// various numbers of clients in each mount. -func TestHandleQuery_MultipleMounts(t *testing.T) { - tests := map[string]struct { - twoMonthsAgo [][]int - oneMonthAgo [][]int - currentMonth [][]int - expectedNewClients int - expectedTotalAccuracy float64 - }{ - "low volume, all mounts": { - twoMonthsAgo: [][]int{ - {20, 20}, - }, - oneMonthAgo: [][]int{ - {30, 30}, - }, - currentMonth: [][]int{ - {40, 40}, - }, - expectedNewClients: 80, - expectedTotalAccuracy: 1, - }, - "medium volume, all mounts": { - twoMonthsAgo: [][]int{ - {200, 200}, - }, - oneMonthAgo: [][]int{ - {300, 300}, - }, - currentMonth: [][]int{ - {400, 400}, - }, - expectedNewClients: 800, - expectedTotalAccuracy: 0.98, - }, - "higher volume, all mounts": { - twoMonthsAgo: [][]int{ - {200, 200}, - }, - oneMonthAgo: [][]int{ - {300, 300}, - }, - currentMonth: [][]int{ - {2000, 5000}, - }, - expectedNewClients: 7000, - expectedTotalAccuracy: 0.95, - }, - "higher volume, no repeats": { - twoMonthsAgo: [][]int{ - {200, 200}, - }, - oneMonthAgo: [][]int{ - {300, 300}, - }, - currentMonth: [][]int{ - {4000, 6000}, - }, - expectedNewClients: 10000, - expectedTotalAccuracy: 0.98, - }, - } - - for i, tt := range tests { - testname := fmt.Sprintf("%s", i) - t.Run(testname, func(t *testing.T) { - var err error - cluster := minimal.NewTestSoloCluster(t, nil) - client := cluster.Cores[0].Client - _, err = client.Logical().Write("sys/internal/counters/config", map[string]interface{}{ - "enabled": "enable", - }) - require.NoError(t, err) - - // Create two namespaces - namespaces := []string{namespace.RootNamespaceID} - mounts := make(map[string][]string) - - // Add two userpass mounts to each namespace - for _, ns := range namespaces { - err = client.WithNamespace(ns).Sys().EnableAuthWithOptions("userpass1", &api.EnableAuthOptions{ - Type: "userpass", - }) - require.NoError(t, err) - err = client.WithNamespace(ns).Sys().EnableAuthWithOptions("userpass2", &api.EnableAuthOptions{ - Type: "userpass", - }) - require.NoError(t, err) - mounts[ns] = []string{"auth/userpass1", "auth/userpass2"} - } - - activityLogGenerator := clientcountutil.NewActivityLogData(client) - - // Write two months ago data - activityLogGenerator = activityLogGenerator.NewPreviousMonthData(2) - for nsIndex, nsId := range namespaces { - for mountIndex, mount := range mounts[nsId] { - activityLogGenerator = activityLogGenerator. - NewClientsSeen(tt.twoMonthsAgo[nsIndex][mountIndex], clientcountutil.WithClientNamespace(nsId), clientcountutil.WithClientMount(mount)) - } - } - - // Write previous months data - activityLogGenerator = activityLogGenerator.NewPreviousMonthData(1) - for nsIndex, nsId := range namespaces { - for mountIndex, mount := range mounts[nsId] { - activityLogGenerator = activityLogGenerator. - NewClientsSeen(tt.oneMonthAgo[nsIndex][mountIndex], clientcountutil.WithClientNamespace(nsId), clientcountutil.WithClientMount(mount)) - } - } - - // Write current month data - activityLogGenerator = activityLogGenerator.NewCurrentMonthData() - for nsIndex, nsPath := range namespaces { - for mountIndex, mount := range mounts[nsPath] { - activityLogGenerator = activityLogGenerator. - RepeatedClientSeen(clientcountutil.WithClientNamespace(nsPath), clientcountutil.WithClientMount(mount)). - NewClientsSeen(tt.currentMonth[nsIndex][mountIndex], clientcountutil.WithClientNamespace(nsPath), clientcountutil.WithClientMount(mount)) - } - } - - // Write all the client count data - _, err = activityLogGenerator.Write(context.Background(), generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES, generation.WriteOptions_WRITE_ENTITIES) - require.NoError(t, err) - - endOfCurrentMonth := timeutil.EndOfMonth(time.Now().UTC()) - - // query activity log - resp, err := client.Logical().ReadWithData("sys/internal/counters/activity", map[string][]string{ - "end_time": {endOfCurrentMonth.Format(time.RFC3339)}, - "start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(2, time.Now().UTC())).Format(time.RFC3339)}, - }) - require.NoError(t, err) - - // Ensure that the month response is the same as the totals, because all clients - // are new clients and there will be no approximation in the single month partial - // case - monthsRaw, ok := resp.Data["months"] - if !ok { - t.Fatalf("malformed results. got %v", resp.Data) - } - monthsResponse := make([]*vault.ResponseMonth, 0) - err = mapstructure.Decode(monthsRaw, &monthsResponse) - - currentMonthClients := monthsResponse[len(monthsResponse)-1] - - // Now verify that the new client totals for ALL namespaces are approximately accurate (there are no namespaces in CE) - newClientsError := math.Abs((float64)(currentMonthClients.NewClients.Counts.Clients - tt.expectedNewClients)) - newClientsErrorMargin := newClientsError / (float64)(tt.expectedNewClients) - expectedAccuracyCalc := (1 - tt.expectedTotalAccuracy) * 100 / 100 - if newClientsErrorMargin > expectedAccuracyCalc { - t.Fatalf("bad accuracy: expected %+v, found %+v", expectedAccuracyCalc, newClientsErrorMargin) - } - - // Verify that the totals for the clients are visibly sensible (that is the total of all the individual new clients per namespace) - total := 0 - for _, newClientCounts := range currentMonthClients.NewClients.Namespaces { - total += newClientCounts.Counts.Clients - } - if diff := math.Abs(float64(currentMonthClients.NewClients.Counts.Clients - total)); diff >= 1 { - t.Fatalf("total expected was %d but got %d", currentMonthClients.NewClients.Counts.Clients, total) - } - }) - } -} diff --git a/vault/logical_system_activity.go b/vault/logical_system_activity.go index 56f9ecb665..41925cb32b 100644 --- a/vault/logical_system_activity.go +++ b/vault/logical_system_activity.go @@ -29,8 +29,19 @@ const ( // WarningCurrentMonthIsAnEstimate is a warning string that is used to let the customer know that for this query, the current month's data is estimated. WarningCurrentMonthIsAnEstimate = "Since this usage period includes both the current month and at least one historical month, counts returned in this usage period are an estimate. Client counts for this period will no longer be estimated at the start of the next month." + + // WarningProvidedStartAndEndTimesIgnored is a warning string that is used to indicate that the provided start and end times by the user have been aligned to a billing period's start and end times + WarningProvidedStartAndEndTimesIgnored = "start_time and end_time parameters can only be used to specify the beginning or end of the same billing period. The values provided for these parameters are not supported and are ignored. Showing the data for the entire billing period corresponding to start_time. If start_time is not provided, the billing period is determined based on the end_time." + + // WarningEndTimeAsCurrentMonthOrFutureIgnored is a warning string that is used to indicate the provided end time has been adjusted to the previous month if it was provided to be within the current month or in future date + WarningEndTimeAsCurrentMonthOrFutureIgnored = "end_time parameter can only be used to specify a date until the end of previous month. The value provided for this parameter was in the current month or in the future date and was therefore ignored. The response includes data until the end of the previous month." ) +type StartEndTimesWarnings struct { + TimesAlignedToBilling bool + EndTimeAdjustedToPastMonth bool +} + // activityQueryPath is available in every namespace func (b *SystemBackend) activityQueryPath() *framework.Path { return &framework.Path{ @@ -332,7 +343,6 @@ func (b *SystemBackend) handleClientExport(ctx context.Context, req *logical.Req } func (b *SystemBackend) handleClientMetricQuery(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - var startTime, endTime time.Time b.Core.activityLogLock.RLock() a := b.Core.activityLog b.Core.activityLogLock.RUnlock() @@ -347,7 +357,8 @@ func (b *SystemBackend) handleClientMetricQuery(ctx context.Context, req *logica } var err error - startTime, endTime, err = parseStartEndTimes(d, b.Core.BillingStart()) + var timeWarnings StartEndTimesWarnings + startTime, endTime, timeWarnings, err := getStartEndTime(d, b.Core.BillingStart()) if err != nil { return logical.ErrorResponse(err.Error()), nil } @@ -371,6 +382,12 @@ func (b *SystemBackend) handleClientMetricQuery(ctx context.Context, req *logica if queryContainsEstimates(startTime, endTime) { warnings = append(warnings, WarningCurrentMonthIsAnEstimate) } + if timeWarnings.EndTimeAdjustedToPastMonth { + warnings = append(warnings, WarningEndTimeAsCurrentMonthOrFutureIgnored) + } + if timeWarnings.TimesAlignedToBilling { + warnings = append(warnings, WarningProvidedStartAndEndTimesIgnored) + } return &logical.Response{ Warnings: warnings,