open-vault/vault/logical_system_activity_write_testonly_test.go
miagilepner 741c890ce0
VAULT-14735: write mock activity log entity files (#20702)
* support writing entities

* tests for writing entity segments
2023-05-25 18:55:55 +02:00

577 lines
18 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
//go:build testonly
package vault
import (
"context"
"sort"
"testing"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault/activity"
"github.com/hashicorp/vault/vault/activity/generation"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
// TestSystemBackend_handleActivityWriteData calls the activity log write endpoint and confirms that the inputs are
// correctly validated
func TestSystemBackend_handleActivityWriteData(t *testing.T) {
testCases := []struct {
name string
operation logical.Operation
input map[string]interface{}
wantError error
wantPaths int
}{
{
name: "read fails",
operation: logical.ReadOperation,
wantError: logical.ErrUnsupportedOperation,
},
{
name: "empty write fails",
operation: logical.CreateOperation,
wantError: logical.ErrInvalidRequest,
},
{
name: "wrong key fails",
operation: logical.CreateOperation,
input: map[string]interface{}{"other": "data"},
wantError: logical.ErrInvalidRequest,
},
{
name: "incorrectly formatted data fails",
operation: logical.CreateOperation,
input: map[string]interface{}{"input": "data"},
wantError: logical.ErrInvalidRequest,
},
{
name: "incorrect json data fails",
operation: logical.CreateOperation,
input: map[string]interface{}{"input": `{"other":"json"}`},
wantError: logical.ErrInvalidRequest,
},
{
name: "empty write value fails",
operation: logical.CreateOperation,
input: map[string]interface{}{"input": `{"write":[],"data":[]}`},
wantError: logical.ErrInvalidRequest,
},
{
name: "empty data value fails",
operation: logical.CreateOperation,
input: map[string]interface{}{"input": `{"write":["WRITE_PRECOMPUTED_QUERIES"],"data":[]}`},
wantError: logical.ErrInvalidRequest,
},
{
name: "correctly formatted data succeeds",
operation: logical.CreateOperation,
input: map[string]interface{}{"input": `{"write":["WRITE_PRECOMPUTED_QUERIES"],"data":[{"current_month":true,"all":{"clients":[{"count":5}]}}]}`},
},
{
name: "entities with multiple segments",
operation: logical.CreateOperation,
input: map[string]interface{}{"input": `{"write":["WRITE_ENTITIES"],"data":[{"current_month":true,"num_segments":3,"all":{"clients":[{"count":5}]}}]}`},
wantPaths: 3,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
b := testSystemBackend(t)
req := logical.TestRequest(t, tc.operation, "internal/counters/activity/write")
req.Data = tc.input
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if tc.wantError != nil {
require.Equal(t, tc.wantError, err, resp.Error())
} else {
require.NoError(t, err)
paths := resp.Data["paths"].([]string)
require.Len(t, paths, tc.wantPaths)
}
})
}
}
// Test_singleMonthActivityClients_addNewClients verifies that new clients are
// created correctly, adhering to the requested parameters. The clients should
// use the inputted mount and a generated ID if one is not supplied. The new
// client should be added to the month's `clients` slice and segment map, if
// a segment index is supplied
func Test_singleMonthActivityClients_addNewClients(t *testing.T) {
segmentIndex := 0
tests := []struct {
name string
mount string
clients *generation.Client
wantNamespace string
wantMount string
wantID string
segmentIndex *int
}{
{
name: "default mount is used",
mount: "default_mount",
wantMount: "default_mount",
clients: &generation.Client{},
},
{
name: "record namespace is used, default mount is used",
mount: "default_mount",
wantNamespace: "ns",
wantMount: "default_mount",
clients: &generation.Client{
Namespace: "ns",
Mount: "mount",
},
},
{
name: "predefined ID is used",
clients: &generation.Client{
Id: "client_id",
},
wantID: "client_id",
},
{
name: "non zero count",
clients: &generation.Client{
Count: 5,
},
},
{
name: "non entity client",
clients: &generation.Client{
NonEntity: true,
},
},
{
name: "added to segment",
clients: &generation.Client{},
segmentIndex: &segmentIndex,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &singleMonthActivityClients{
predefinedSegments: make(map[int][]int),
}
err := m.addNewClients(tt.clients, tt.mount, tt.segmentIndex)
require.NoError(t, err)
numNew := tt.clients.Count
if numNew == 0 {
numNew = 1
}
require.Len(t, m.clients, int(numNew))
for i, rec := range m.clients {
require.NotNil(t, rec)
require.Equal(t, tt.wantNamespace, rec.NamespaceID)
require.Equal(t, tt.wantMount, rec.MountAccessor)
require.Equal(t, tt.clients.NonEntity, rec.NonEntity)
if tt.wantID != "" {
require.Equal(t, tt.wantID, rec.ClientID)
} else {
require.NotEqual(t, "", rec.ClientID)
}
if tt.segmentIndex != nil {
require.Contains(t, m.predefinedSegments[*tt.segmentIndex], i)
}
}
})
}
}
// Test_multipleMonthsActivityClients_processMonth verifies that a month of data
// is added correctly. The test checks that default values are handled correctly
// for mounts and namespaces.
func Test_multipleMonthsActivityClients_processMonth(t *testing.T) {
core, _, _ := TestCoreUnsealed(t)
tests := []struct {
name string
clients *generation.Data
wantError bool
numMonths int
}{
{
name: "specified namespace and mount exist",
clients: &generation.Data{
Clients: &generation.Data_All{All: &generation.Clients{Clients: []*generation.Client{{
Namespace: namespace.RootNamespaceID,
Mount: "identity/",
}}}},
},
numMonths: 1,
},
{
name: "specified namespace exists, mount empty",
clients: &generation.Data{
Clients: &generation.Data_All{All: &generation.Clients{Clients: []*generation.Client{{
Namespace: namespace.RootNamespaceID,
}}}},
},
numMonths: 1,
},
{
name: "empty namespace and mount",
clients: &generation.Data{
Clients: &generation.Data_All{All: &generation.Clients{Clients: []*generation.Client{{}}}},
},
numMonths: 1,
},
{
name: "namespace doesn't exist",
clients: &generation.Data{
Clients: &generation.Data_All{All: &generation.Clients{Clients: []*generation.Client{{
Namespace: "abcd",
}}}},
},
wantError: true,
numMonths: 1,
},
{
name: "namespace exists, mount doesn't exist",
clients: &generation.Data{
Clients: &generation.Data_All{All: &generation.Clients{Clients: []*generation.Client{{
Namespace: namespace.RootNamespaceID,
Mount: "mount",
}}}},
},
wantError: true,
numMonths: 1,
},
{
name: "older month",
clients: &generation.Data{
Month: &generation.Data_MonthsAgo{MonthsAgo: 4},
Clients: &generation.Data_All{All: &generation.Clients{Clients: []*generation.Client{{}}}},
},
numMonths: 5,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := newMultipleMonthsActivityClients(tt.numMonths)
err := m.processMonth(context.Background(), core, tt.clients)
if tt.wantError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Len(t, m.months[tt.clients.GetMonthsAgo()].clients, len(tt.clients.GetAll().Clients))
for _, month := range m.months {
for _, c := range month.clients {
require.NotEmpty(t, c.NamespaceID)
require.NotEmpty(t, c.MountAccessor)
}
}
}
})
}
}
// Test_multipleMonthsActivityClients_processMonth_segmented verifies that segments
// are filled correctly when a month is processed with segmented data. The clients
// should be in the clients array, and should also be in the predefinedSegments map
// at the correct segment index
func Test_multipleMonthsActivityClients_processMonth_segmented(t *testing.T) {
index7 := int32(7)
data := &generation.Data{
Clients: &generation.Data_Segments{
Segments: &generation.Segments{
Segments: []*generation.Segment{
{
Clients: &generation.Clients{Clients: []*generation.Client{
{},
}},
},
{
Clients: &generation.Clients{Clients: []*generation.Client{{}}},
},
{
SegmentIndex: &index7,
Clients: &generation.Clients{Clients: []*generation.Client{{}}},
},
},
},
},
}
m := newMultipleMonthsActivityClients(1)
core, _, _ := TestCoreUnsealed(t)
require.NoError(t, m.processMonth(context.Background(), core, data))
require.Len(t, m.months[0].predefinedSegments, 3)
require.Len(t, m.months[0].clients, 3)
// segment indexes are correct
require.Contains(t, m.months[0].predefinedSegments, 0)
require.Contains(t, m.months[0].predefinedSegments, 1)
require.Contains(t, m.months[0].predefinedSegments, 7)
// the data in each segment is correct
require.Contains(t, m.months[0].predefinedSegments[0], 0)
require.Contains(t, m.months[0].predefinedSegments[1], 1)
require.Contains(t, m.months[0].predefinedSegments[7], 2)
}
// Test_multipleMonthsActivityClients_addRepeatedClients adds repeated clients
// 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) {
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, NonEntity: true}, defaultMount, nil))
month2Clients := m.months[2].clients
month1Clients := m.months[1].clients
thisMonth := m.months[0]
// this will match the first client in month 1
require.NoError(t, m.addRepeatedClients(0, &generation.Client{Count: 1, Repeated: true}, defaultMount, nil))
require.Contains(t, month1Clients, thisMonth.clients[0])
// this will match the 3rd client in month 1
require.NoError(t, m.addRepeatedClients(0, &generation.Client{Count: 1, Repeated: true, NonEntity: true}, defaultMount, nil))
require.Equal(t, month1Clients[2], thisMonth.clients[1])
// this will match the first two clients in month 1
require.NoError(t, m.addRepeatedClients(0, &generation.Client{Count: 2, Repeated: true}, defaultMount, nil))
require.Equal(t, month1Clients[0:2], thisMonth.clients[2:4])
// this will match the first client in month 2
require.NoError(t, m.addRepeatedClients(0, &generation.Client{Count: 1, RepeatedFromMonth: 2}, "identity", nil))
require.Equal(t, month2Clients[0], thisMonth.clients[4])
// this will match the 3rd client in month 2
require.NoError(t, m.addRepeatedClients(0, &generation.Client{Count: 1, RepeatedFromMonth: 2, Namespace: "other_ns"}, defaultMount, nil))
require.Equal(t, month2Clients[2], thisMonth.clients[5])
require.Error(t, m.addRepeatedClients(0, &generation.Client{Count: 1, RepeatedFromMonth: 2, Namespace: "other_ns"}, "other_mount", nil))
}
// Test_singleMonthActivityClients_populateSegments calls populateSegments for a
// collection of 5 clients, segmented in various ways. The test ensures that the
// resulting map has the correct clients for each segment index
func Test_singleMonthActivityClients_populateSegments(t *testing.T) {
clients := []*activity.EntityRecord{
{ClientID: "a"},
{ClientID: "b"},
{ClientID: "c"},
{ClientID: "d"},
{ClientID: "e"},
}
cases := []struct {
name string
segments map[int][]int
numSegments int
emptyIndexes []int32
skipIndexes []int32
wantSegments map[int][]*activity.EntityRecord
}{
{
name: "segmented",
segments: map[int][]int{
0: {0, 1},
1: {2, 3},
2: {4},
},
wantSegments: map[int][]*activity.EntityRecord{
0: {{ClientID: "a"}, {ClientID: "b"}},
1: {{ClientID: "c"}, {ClientID: "d"}},
2: {{ClientID: "e"}},
},
},
{
name: "segmented with skip and empty",
segments: map[int][]int{
0: {0, 1},
2: {0, 1},
},
emptyIndexes: []int32{1, 4},
skipIndexes: []int32{3},
wantSegments: map[int][]*activity.EntityRecord{
0: {{ClientID: "a"}, {ClientID: "b"}},
1: {},
2: {{ClientID: "a"}, {ClientID: "b"}},
3: nil,
4: {},
},
},
{
name: "all clients",
numSegments: 0,
wantSegments: map[int][]*activity.EntityRecord{
0: {{ClientID: "a"}, {ClientID: "b"}, {ClientID: "c"}, {ClientID: "d"}, {ClientID: "e"}},
},
},
{
name: "all clients split",
numSegments: 2,
wantSegments: map[int][]*activity.EntityRecord{
0: {{ClientID: "a"}, {ClientID: "b"}, {ClientID: "c"}},
1: {{ClientID: "d"}, {ClientID: "e"}},
},
},
{
name: "all clients with skip and empty",
numSegments: 5,
skipIndexes: []int32{0, 3},
emptyIndexes: []int32{2},
wantSegments: map[int][]*activity.EntityRecord{
0: nil,
1: {{ClientID: "a"}, {ClientID: "b"}, {ClientID: "c"}},
2: {},
3: nil,
4: {{ClientID: "d"}, {ClientID: "e"}},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
s := singleMonthActivityClients{predefinedSegments: tc.segments, clients: clients, generationParameters: &generation.Data{EmptySegmentIndexes: tc.emptyIndexes, SkipSegmentIndexes: tc.skipIndexes, NumSegments: int32(tc.numSegments)}}
gotSegments, err := s.populateSegments()
require.NoError(t, err)
require.Equal(t, tc.wantSegments, gotSegments)
})
}
}
// Test_multipleMonthsActivityClients_write_entities writes 4 months of data
// splitting some months across segments and using empty segments and skipped
// segments. Entities are written and then storage is queried. The test verifies
// that the correct timestamps are present in the activity log and that the correct
// segment numbers for each month contain the correct number of clients
func Test_multipleMonthsActivityClients_write_entities(t *testing.T) {
index5 := int32(5)
index4 := int32(4)
data := &generation.ActivityLogMockInput{
Write: []generation.WriteOptions{
generation.WriteOptions_WRITE_ENTITIES,
},
Data: []*generation.Data{
{
// segments: 0:[x,y], 1:[z]
Month: &generation.Data_MonthsAgo{MonthsAgo: 3},
Clients: &generation.Data_All{All: &generation.Clients{Clients: []*generation.Client{{Count: 3}}}},
NumSegments: 2,
},
{
// segments: 1:[a,b,c], 2:[d,e]
Month: &generation.Data_MonthsAgo{MonthsAgo: 2},
Clients: &generation.Data_All{All: &generation.Clients{Clients: []*generation.Client{{Count: 5}}}},
NumSegments: 3,
SkipSegmentIndexes: []int32{0},
},
{
// segments: 5:[f,g]
Month: &generation.Data_MonthsAgo{MonthsAgo: 1},
Clients: &generation.Data_Segments{
Segments: &generation.Segments{Segments: []*generation.Segment{{
SegmentIndex: &index5,
Clients: &generation.Clients{Clients: []*generation.Client{{Count: 2}}},
}}},
},
},
{
// segments: 1:[], 2:[], 4:[n], 5:[o]
Month: &generation.Data_CurrentMonth{},
EmptySegmentIndexes: []int32{1, 2},
Clients: &generation.Data_Segments{
Segments: &generation.Segments{Segments: []*generation.Segment{
{
SegmentIndex: &index5,
Clients: &generation.Clients{Clients: []*generation.Client{{Count: 1}}},
},
{
SegmentIndex: &index4,
Clients: &generation.Clients{Clients: []*generation.Client{{Count: 1}}},
},
}},
},
},
},
}
core, _, _ := TestCoreUnsealed(t)
marshaled, err := protojson.Marshal(data)
require.NoError(t, err)
req := logical.TestRequest(t, logical.CreateOperation, "internal/counters/activity/write")
req.Data = map[string]interface{}{"input": string(marshaled)}
resp, err := core.systemBackend.HandleRequest(namespace.RootContext(nil), req)
require.NoError(t, err)
paths := resp.Data["paths"].([]string)
require.Len(t, paths, 9)
times, err := core.activityLog.availableLogs(context.Background())
require.NoError(t, err)
require.Len(t, times, 4)
sortPaths := func(monthPaths []string) {
sort.Slice(monthPaths, func(i, j int) bool {
iVal, _ := parseSegmentNumberFromPath(monthPaths[i])
jVal, _ := parseSegmentNumberFromPath(monthPaths[j])
return iVal < jVal
})
}
month0Paths := paths[0:4]
month1Paths := paths[4:5]
month2Paths := paths[5:7]
month3Paths := paths[7:9]
sortPaths(month0Paths)
sortPaths(month1Paths)
sortPaths(month2Paths)
sortPaths(month3Paths)
entities := func(paths []string) map[int][]*activity.EntityRecord {
segments := make(map[int][]*activity.EntityRecord)
for _, path := range paths {
segmentNum, _ := parseSegmentNumberFromPath(path)
entry, err := core.activityLog.view.Get(context.Background(), path)
require.NoError(t, err)
if entry == nil {
segments[segmentNum] = []*activity.EntityRecord{}
continue
}
activities := &activity.EntityActivityLog{}
err = proto.Unmarshal(entry.Value, activities)
require.NoError(t, err)
segments[segmentNum] = activities.Clients
}
return segments
}
month0Entities := entities(month0Paths)
require.Len(t, month0Entities, 4)
require.Contains(t, month0Entities, 1)
require.Contains(t, month0Entities, 2)
require.Contains(t, month0Entities, 4)
require.Contains(t, month0Entities, 5)
require.Len(t, month0Entities[1], 0)
require.Len(t, month0Entities[2], 0)
require.Len(t, month0Entities[4], 1)
require.Len(t, month0Entities[5], 1)
month1Entities := entities(month1Paths)
require.Len(t, month1Entities, 1)
require.Contains(t, month1Entities, 5)
require.Len(t, month1Entities[5], 2)
month2Entities := entities(month2Paths)
require.Len(t, month2Entities, 2)
require.Contains(t, month2Entities, 1)
require.Contains(t, month2Entities, 2)
require.Len(t, month2Entities[1], 3)
require.Len(t, month2Entities[2], 2)
month3Entities := entities(month3Paths)
require.Len(t, month3Entities, 2)
require.Contains(t, month3Entities, 0)
require.Contains(t, month3Entities, 1)
require.Len(t, month3Entities[0], 2)
require.Len(t, month3Entities[1], 1)
}