Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add non-root access with filtering support to activity export API #27846

Merged
merged 9 commits into from
Jul 24, 2024
Prev Previous commit
Next Next commit
add test to verify sudo access
  • Loading branch information
ccapurso committed Jul 23, 2024
commit 5f78d145acf278c82b11025cf29f6f54f7897772
160 changes: 160 additions & 0 deletions vault/external_tests/activity_testonly/activity_testonly_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
package activity_testonly

import (
"bytes"
"context"
"encoding/csv"
"encoding/json"
"fmt"
"io"
"math"
"testing"
"time"
Expand Down Expand Up @@ -451,6 +454,163 @@ func Test_ActivityLog_MountDeduplication(t *testing.T) {
}, mountSet)
}

// getJSONExport is used to fetch activity export records using json format.
// The records will returned as a map keyed by client ID.
func getJSONExport(t *testing.T, client *api.Client, monthsPreviousTo int, now time.Time) (map[string]vault.ActivityLogExportRecord, error) {
t.Helper()

resp, err := client.Logical().ReadRawWithData("sys/internal/counters/activity/export", map[string][]string{
"start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(monthsPreviousTo, now)).Format(time.RFC3339)},
"end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)},
"format": {"json"},
})
if err != nil {
return nil, err
}

defer resp.Body.Close()

contents, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

buf := bytes.NewBuffer(contents)
decoder := json.NewDecoder(buf)
clients := make(map[string]vault.ActivityLogExportRecord)

for {

var record vault.ActivityLogExportRecord
err := decoder.Decode(&record)
if err != nil {
return nil, err
}

clients[record.ClientID] = record

if !decoder.More() {
break
}
}

return clients, nil
}

// getCSVExport is used to fetch activity export records using csv format.
// The records will returned as a map keyed by client ID.
func getCSVExport(t *testing.T, client *api.Client, monthsPreviousTo int, now time.Time) (map[string]vault.ActivityLogExportRecord, error) {
t.Helper()

resp, err := client.Logical().ReadRawWithData("sys/internal/counters/activity/export", map[string][]string{
"start_time": {timeutil.StartOfMonth(timeutil.MonthsPreviousTo(monthsPreviousTo, now)).Format(time.RFC3339)},
"end_time": {timeutil.EndOfMonth(now).Format(time.RFC3339)},
"format": {"csv"},
})
if err != nil {
return nil, err
}

defer resp.Body.Close()

csvRdr := csv.NewReader(resp.Body)
clients := make(map[string]vault.ActivityLogExportRecord)

csvRecords, err := csvRdr.ReadAll()
if err != nil {
return nil, err
}

csvColumns := csvRecords[0]

for i := 1; i < len(csvRecords); i++ {
recordMap := make(map[string]interface{})

for j, k := range csvColumns {
recordMap[k] = csvRecords[i][j]
}

var record vault.ActivityLogExportRecord
err = mapstructure.Decode(recordMap, &record)
if err != nil {
return nil, err
}

clients[record.ClientID] = record
}

return clients, nil
}

// Test_ActivityLog_Export_Sudo ensures that the export API is only accessible via
// a root token or a token with a sudo policy.
func Test_ActivityLog_Export_Sudo(t *testing.T) {
timeutil.SkipAtEndOfMonth(t)
t.Parallel()

now := time.Now().UTC()
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)

rootToken := client.Token()

_, err = clientcountutil.NewActivityLogData(client).
NewCurrentMonthData().
NewClientsSeen(10).
Write(context.Background(), generation.WriteOptions_WRITE_ENTITIES)

require.NoError(t, err)

// Ensure access via root token
clients, err := getJSONExport(t, client, 1, now)
require.NoError(t, err)
require.Len(t, clients, 10)

client.Sys().PutPolicy("non-sudo-export", `
path "sys/internal/counters/activity/export" {
capabilities = ["read"]
}
`)

secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{
Policies: []string{"non-sudo-export"},
})
require.NoError(t, err)

nonSudoToken := secret.Auth.ClientToken
client.SetToken(nonSudoToken)

// Ensure no access via token without sudo access
clients, err = getJSONExport(t, client, 1, now)
require.ErrorContains(t, err, "permission denied")

client.SetToken(rootToken)
client.Sys().PutPolicy("sudo-export", `
path "sys/internal/counters/activity/export" {
capabilities = ["read", "sudo"]
}
`)

secret, err = client.Auth().Token().Create(&api.TokenCreateRequest{
Policies: []string{"sudo-export"},
})
require.NoError(t, err)

sudoToken := secret.Auth.ClientToken
client.SetToken(sudoToken)

// Ensure access via token with sudo access
clients, err = getJSONExport(t, client, 1, now)
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
Expand Down
22 changes: 13 additions & 9 deletions vault/logical_system_activity_write_testonly.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ func (b *SystemBackend) activityWritePath() *framework.Path {
}

func (b *SystemBackend) handleActivityWriteData(ctx context.Context, request *logical.Request, data *framework.FieldData) (*logical.Response, error) {
now := time.Now().UTC()

json := data.Get("input")
input := &generation.ActivityLogMockInput{}
err := protojson.Unmarshal([]byte(json.(string)), input)
Expand All @@ -73,7 +75,7 @@ func (b *SystemBackend) handleActivityWriteData(ctx context.Context, request *lo
}
generated := newMultipleMonthsActivityClients(numMonths + 1)
for _, month := range input.Data {
err := generated.processMonth(ctx, b.Core, month)
err := generated.processMonth(ctx, b.Core, month, now)
if err != nil {
return logical.ErrorResponse("failed to process data for month %d", month.GetMonthsAgo()), err
}
Expand All @@ -83,7 +85,7 @@ func (b *SystemBackend) handleActivityWriteData(ctx context.Context, request *lo
for _, opt := range input.Write {
opts[opt] = struct{}{}
}
paths, err := generated.write(ctx, opts, b.Core.activityLog)
paths, err := generated.write(ctx, opts, b.Core.activityLog, now)
if err != nil {
b.logger.Debug("failed to write activity log data", "error", err.Error())
return logical.ErrorResponse("failed to write data"), err
Expand Down Expand Up @@ -185,19 +187,22 @@ func (s *singleMonthActivityClients) populateSegments() (map[int][]*activity.Ent

// addNewClients generates clients according to the given parameters, and adds them to the month
// the client will always have the mountAccessor as its mount accessor
func (s *singleMonthActivityClients) addNewClients(c *generation.Client, mountAccessor string, segmentIndex *int) error {
func (s *singleMonthActivityClients) addNewClients(c *generation.Client, mountAccessor string, segmentIndex *int, monthsAgo int32, now time.Time) error {
count := 1
if c.Count > 1 {
count = int(c.Count)
}
isNonEntity := c.ClientType != entityActivityType
ts := timeutil.MonthsPreviousTo(int(monthsAgo), now)

for i := 0; i < count; i++ {
record := &activity.EntityRecord{
ClientID: c.Id,
NamespaceID: c.Namespace,
MountAccessor: mountAccessor,
NonEntity: isNonEntity,
ClientType: c.ClientType,
Timestamp: ts.Unix(),
}
if record.ClientID == "" {
var err error
Expand All @@ -212,7 +217,7 @@ func (s *singleMonthActivityClients) addNewClients(c *generation.Client, mountAc
}

// processMonth populates a month of client data
func (m *multipleMonthsActivityClients) processMonth(ctx context.Context, core *Core, month *generation.Data) error {
func (m *multipleMonthsActivityClients) processMonth(ctx context.Context, core *Core, month *generation.Data, now time.Time) error {
// default to using the root namespace and the first mount on the root namespace
mounts, err := core.ListMounts()
if err != nil {
Expand Down Expand Up @@ -275,7 +280,7 @@ func (m *multipleMonthsActivityClients) processMonth(ctx context.Context, core *
}
}

err = m.addClientToMonth(month.GetMonthsAgo(), clients, mountAccessor, segmentIndex)
err = m.addClientToMonth(month.GetMonthsAgo(), clients, mountAccessor, segmentIndex, now)
if err != nil {
return err
}
Expand All @@ -301,11 +306,11 @@ func (m *multipleMonthsActivityClients) processMonth(ctx context.Context, core *
return nil
}

func (m *multipleMonthsActivityClients) addClientToMonth(monthsAgo int32, c *generation.Client, mountAccessor string, segmentIndex *int) error {
func (m *multipleMonthsActivityClients) addClientToMonth(monthsAgo int32, c *generation.Client, mountAccessor string, segmentIndex *int, now time.Time) error {
if c.Repeated || c.RepeatedFromMonth > 0 {
return m.addRepeatedClients(monthsAgo, c, mountAccessor, segmentIndex)
}
return m.months[monthsAgo].addNewClients(c, mountAccessor, segmentIndex)
return m.months[monthsAgo].addNewClients(c, mountAccessor, segmentIndex, monthsAgo, now)
}

func (m *multipleMonthsActivityClients) addRepeatedClients(monthsAgo int32, c *generation.Client, mountAccessor string, segmentIndex *int) error {
Expand Down Expand Up @@ -351,8 +356,7 @@ func (m *multipleMonthsActivityClients) timestampForMonth(i int, now time.Time)
return now
}

func (m *multipleMonthsActivityClients) write(ctx context.Context, opts map[generation.WriteOptions]struct{}, activityLog *ActivityLog) ([]string, error) {
now := time.Now().UTC()
func (m *multipleMonthsActivityClients) write(ctx context.Context, opts map[generation.WriteOptions]struct{}, activityLog *ActivityLog, now time.Time) ([]string, error) {
paths := []string{}

_, writePQ := opts[generation.WriteOptions_WRITE_PRECOMPUTED_QUERIES]
Expand Down
16 changes: 9 additions & 7 deletions vault/logical_system_activity_write_testonly_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func Test_singleMonthActivityClients_addNewClients(t *testing.T) {
m := &singleMonthActivityClients{
predefinedSegments: make(map[int][]int),
}
err := m.addNewClients(tt.clients, tt.mount, tt.segmentIndex)
err := m.addNewClients(tt.clients, tt.mount, tt.segmentIndex, 0, time.Now().UTC())
require.NoError(t, err)
numNew := tt.clients.Count
if numNew == 0 {
Expand Down Expand Up @@ -275,7 +275,7 @@ func Test_multipleMonthsActivityClients_processMonth(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := newMultipleMonthsActivityClients(tt.numMonths)
err := m.processMonth(context.Background(), core, tt.clients)
err := m.processMonth(context.Background(), core, tt.clients, time.Now().UTC())
if tt.wantError {
require.Error(t, err)
} else {
Expand Down Expand Up @@ -320,7 +320,7 @@ func Test_multipleMonthsActivityClients_processMonth_segmented(t *testing.T) {
}
m := newMultipleMonthsActivityClients(1)
core, _, _ := TestCoreUnsealed(t)
require.NoError(t, m.processMonth(context.Background(), core, data))
require.NoError(t, m.processMonth(context.Background(), core, data, time.Now().UTC()))
require.Len(t, m.months[0].predefinedSegments, 3)
require.Len(t, m.months[0].clients, 3)

Expand All @@ -339,13 +339,15 @@ func Test_multipleMonthsActivityClients_processMonth_segmented(t *testing.T) {
// from 1 month ago and 2 months ago, and verifies that the correct clients are
// added based on namespace, mount, and non-entity attributes
func Test_multipleMonthsActivityClients_addRepeatedClients(t *testing.T) {
now := time.Now().UTC()

m := newMultipleMonthsActivityClients(3)
defaultMount := "default"

require.NoError(t, m.addClientToMonth(2, &generation.Client{Count: 2}, "identity", nil))
require.NoError(t, m.addClientToMonth(2, &generation.Client{Count: 2, Namespace: "other_ns"}, defaultMount, nil))
require.NoError(t, m.addClientToMonth(1, &generation.Client{Count: 2}, defaultMount, nil))
require.NoError(t, m.addClientToMonth(1, &generation.Client{Count: 2, ClientType: "non-entity"}, defaultMount, nil))
require.NoError(t, m.addClientToMonth(2, &generation.Client{Count: 2}, "identity", nil, now))
require.NoError(t, m.addClientToMonth(2, &generation.Client{Count: 2, Namespace: "other_ns"}, defaultMount, nil, now))
require.NoError(t, m.addClientToMonth(1, &generation.Client{Count: 2}, defaultMount, nil, now))
require.NoError(t, m.addClientToMonth(1, &generation.Client{Count: 2, ClientType: "non-entity"}, defaultMount, nil, now))

month2Clients := m.months[2].clients
month1Clients := m.months[1].clients
Expand Down
Loading