353 lines
8.5 KiB
Go
353 lines
8.5 KiB
Go
|
// Copyright (c) HashiCorp, Inc.
|
||
|
// SPDX-License-Identifier: MPL-2.0
|
||
|
|
||
|
package vault
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
"github.com/armon/go-metrics"
|
||
|
logicalKv "github.com/hashicorp/vault-plugin-secrets-kv"
|
||
|
"github.com/hashicorp/vault/helper/namespace"
|
||
|
"github.com/hashicorp/vault/sdk/logical"
|
||
|
)
|
||
|
|
||
|
func TestCoreMetrics_KvSecretGauge(t *testing.T) {
|
||
|
// Use the real KV implementation instead of Passthrough
|
||
|
AddTestLogicalBackend("kv", logicalKv.Factory)
|
||
|
// Clean up for the next test-- is there a better way?
|
||
|
defer func() {
|
||
|
delete(testLogicalBackends, "kv")
|
||
|
}()
|
||
|
core, _, root := TestCoreUnsealed(t)
|
||
|
|
||
|
testMounts := []struct {
|
||
|
Path string
|
||
|
Type string
|
||
|
Version string
|
||
|
ExpectedCount int
|
||
|
}{
|
||
|
{"secret/", "kv", "2", 0},
|
||
|
{"secret1/", "kv", "1", 3},
|
||
|
{"secret2/", "kv", "1", 0},
|
||
|
{"secret3/", "kv", "2", 4},
|
||
|
{"prefix/secret3/", "kv", "2", 0},
|
||
|
{"prefix/secret4/", "kv", "2", 5},
|
||
|
{"generic/", "generic", "1", 3},
|
||
|
}
|
||
|
ctx := namespace.RootContext(nil)
|
||
|
|
||
|
// skip 0, secret/ is already mounted
|
||
|
for _, tm := range testMounts[1:] {
|
||
|
me := &MountEntry{
|
||
|
Table: mountTableType,
|
||
|
Path: sanitizePath(tm.Path),
|
||
|
Type: tm.Type,
|
||
|
Options: map[string]string{"version": tm.Version},
|
||
|
}
|
||
|
err := core.mount(ctx, me)
|
||
|
if err != nil {
|
||
|
t.Fatalf("err: %v", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
v1secrets := []string{
|
||
|
"secret1/a", // 3
|
||
|
"secret1/b",
|
||
|
"secret1/c/d",
|
||
|
"generic/a",
|
||
|
"generic/b",
|
||
|
"generic/c",
|
||
|
}
|
||
|
v2secrets := []string{
|
||
|
"secret3/data/a", // 4
|
||
|
"secret3/data/b",
|
||
|
"secret3/data/c/d",
|
||
|
"secret3/data/c/e",
|
||
|
"prefix/secret4/data/a/secret", // 5
|
||
|
"prefix/secret4/data/a/secret2",
|
||
|
"prefix/secret4/data/a/b/c/secret",
|
||
|
"prefix/secret4/data/a/b/c/secret2",
|
||
|
"prefix/secret4/data/a/b/c/d/secret3",
|
||
|
}
|
||
|
for _, p := range v1secrets {
|
||
|
req := logical.TestRequest(t, logical.CreateOperation, p)
|
||
|
req.Data["foo"] = "bar"
|
||
|
req.ClientToken = root
|
||
|
resp, err := core.HandleRequest(ctx, req)
|
||
|
if err != nil {
|
||
|
t.Fatalf("err: %v", err)
|
||
|
}
|
||
|
if resp != nil {
|
||
|
t.Fatalf("bad: %#v", resp)
|
||
|
}
|
||
|
}
|
||
|
for _, p := range v2secrets {
|
||
|
for i := 0; i < 50; i++ {
|
||
|
req := logical.TestRequest(t, logical.CreateOperation, p)
|
||
|
req.Data["data"] = map[string]interface{}{"foo": "bar"}
|
||
|
req.ClientToken = root
|
||
|
resp, err := core.HandleRequest(ctx, req)
|
||
|
if err != nil {
|
||
|
if errors.Is(err, logical.ErrInvalidRequest) {
|
||
|
// Handle scenario where KVv2 upgrade is ongoing
|
||
|
time.Sleep(100 * time.Millisecond)
|
||
|
continue
|
||
|
}
|
||
|
t.Fatalf("err: %v", err)
|
||
|
}
|
||
|
if resp.Error() != nil {
|
||
|
t.Fatalf("bad: %#v", resp)
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
values, err := core.kvSecretGaugeCollector(ctx)
|
||
|
if err != nil {
|
||
|
t.Fatalf("err: %v", err)
|
||
|
}
|
||
|
if len(values) != len(testMounts) {
|
||
|
t.Errorf("Got %v values but expected %v mounts", len(values), len(testMounts))
|
||
|
}
|
||
|
|
||
|
for _, glv := range values {
|
||
|
mountPoint := ""
|
||
|
for _, l := range glv.Labels {
|
||
|
if l.Name == "mount_point" {
|
||
|
mountPoint = l.Value
|
||
|
} else if l.Name == "namespace" {
|
||
|
if l.Value != "root" {
|
||
|
t.Errorf("Namespace is %v, not root", l.Value)
|
||
|
}
|
||
|
} else {
|
||
|
t.Errorf("Unexpected label %v", l.Name)
|
||
|
}
|
||
|
}
|
||
|
if mountPoint == "" {
|
||
|
t.Errorf("No mount point in labels %v", glv.Labels)
|
||
|
continue
|
||
|
}
|
||
|
found := false
|
||
|
for _, tm := range testMounts {
|
||
|
if tm.Path == mountPoint {
|
||
|
found = true
|
||
|
if glv.Value != float32(tm.ExpectedCount) {
|
||
|
t.Errorf("Mount %v reported %v, not %v",
|
||
|
tm.Path, glv.Value, tm.ExpectedCount)
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if !found {
|
||
|
t.Errorf("Unexpected mount point %v", mountPoint)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestCoreMetrics_KvSecretGauge_BadPath(t *testing.T) {
|
||
|
// Use the real KV implementation instead of Passthrough
|
||
|
AddTestLogicalBackend("kv", logicalKv.Factory)
|
||
|
// Clean up for the next test.
|
||
|
defer func() {
|
||
|
delete(testLogicalBackends, "kv")
|
||
|
}()
|
||
|
core, _, _ := TestCoreUnsealed(t)
|
||
|
|
||
|
me := &MountEntry{
|
||
|
Table: mountTableType,
|
||
|
Path: sanitizePath("kv1"),
|
||
|
Type: "kv",
|
||
|
Options: map[string]string{"version": "1"},
|
||
|
}
|
||
|
ctx := namespace.RootContext(nil)
|
||
|
err := core.mount(ctx, me)
|
||
|
if err != nil {
|
||
|
t.Fatalf("mount error: %v", err)
|
||
|
}
|
||
|
|
||
|
// I don't think there's any remaining way to create a zero-length
|
||
|
// key via the API, so we'll fake it by talking to the storage layer directly.
|
||
|
fake_entry := &logical.StorageEntry{
|
||
|
Key: "logical/" + me.UUID + "/foo/",
|
||
|
Value: []byte{1},
|
||
|
}
|
||
|
err = core.barrier.Put(ctx, fake_entry)
|
||
|
if err != nil {
|
||
|
t.Fatalf("put error: %v", err)
|
||
|
}
|
||
|
|
||
|
values, err := core.kvSecretGaugeCollector(ctx)
|
||
|
if err != nil {
|
||
|
t.Fatalf("collector error: %v", err)
|
||
|
}
|
||
|
t.Logf("Values: %v", values)
|
||
|
found := false
|
||
|
var count float32 = -1
|
||
|
for _, glv := range values {
|
||
|
for _, l := range glv.Labels {
|
||
|
if l.Name == "mount_point" && l.Value == "kv1/" {
|
||
|
found = true
|
||
|
count = glv.Value
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if found {
|
||
|
if count != 1.0 {
|
||
|
t.Errorf("bad secret count for kv1/")
|
||
|
}
|
||
|
} else {
|
||
|
t.Errorf("no secret count for kv1/")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestCoreMetrics_KvSecretGaugeError(t *testing.T) {
|
||
|
core, _, _, sink := TestCoreUnsealedWithMetrics(t)
|
||
|
ctx := namespace.RootContext(nil)
|
||
|
|
||
|
badKvMount := &kvMount{
|
||
|
Namespace: namespace.RootNamespace,
|
||
|
MountPoint: "bad/path",
|
||
|
Version: "1",
|
||
|
NumSecrets: 0,
|
||
|
}
|
||
|
|
||
|
core.walkKvMountSecrets(ctx, badKvMount)
|
||
|
|
||
|
intervals := sink.Data()
|
||
|
// Test crossed an interval boundary, don't try to deal with it.
|
||
|
if len(intervals) > 1 {
|
||
|
t.Skip("Detected interval crossing.")
|
||
|
}
|
||
|
|
||
|
// Should be an error
|
||
|
keyPrefix := "metrics.collection.error"
|
||
|
var counter *metrics.SampledValue = nil
|
||
|
|
||
|
for _, c := range intervals[0].Counters {
|
||
|
if strings.HasPrefix(c.Name, keyPrefix) {
|
||
|
counter = &c
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if counter == nil {
|
||
|
t.Fatal("No metrics.collection.error counter found.")
|
||
|
}
|
||
|
if counter.Count != 1 {
|
||
|
t.Errorf("Counter number of samples %v is not 1.", counter.Count)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func metricLabelsMatch(t *testing.T, actual []metrics.Label, expected map[string]string) {
|
||
|
t.Helper()
|
||
|
|
||
|
if len(actual) != len(expected) {
|
||
|
t.Errorf("Expected %v labels, got %v: %v", len(expected), len(actual), actual)
|
||
|
}
|
||
|
|
||
|
for _, l := range actual {
|
||
|
if v, ok := expected[l.Name]; ok {
|
||
|
if v != l.Value {
|
||
|
t.Errorf("Mismatched value %v=%v, expected %v", l.Name, l.Value, v)
|
||
|
}
|
||
|
} else {
|
||
|
t.Errorf("Unexpected label %v", l.Name)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestCoreMetrics_EntityGauges(t *testing.T) {
|
||
|
ctx := namespace.RootContext(nil)
|
||
|
is, ghAccessor, upAccessor, core := testIdentityStoreWithGithubUserpassAuth(ctx, t)
|
||
|
|
||
|
// Create an entity
|
||
|
alias1 := &logical.Alias{
|
||
|
MountType: "github",
|
||
|
MountAccessor: ghAccessor,
|
||
|
Name: "githubuser",
|
||
|
}
|
||
|
|
||
|
entity, _, err := is.CreateOrFetchEntity(ctx, alias1)
|
||
|
if err != nil {
|
||
|
t.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Create a second alias for the same entity
|
||
|
registerReq := &logical.Request{
|
||
|
Operation: logical.UpdateOperation,
|
||
|
Path: "entity-alias",
|
||
|
Data: map[string]interface{}{
|
||
|
"name": "userpassuser",
|
||
|
"canonical_id": entity.ID,
|
||
|
"mount_accessor": upAccessor,
|
||
|
},
|
||
|
}
|
||
|
resp, err := is.HandleRequest(ctx, registerReq)
|
||
|
if err != nil || (resp != nil && resp.IsError()) {
|
||
|
t.Fatalf("err:%v resp:%#v", err, resp)
|
||
|
}
|
||
|
|
||
|
glv, err := core.entityGaugeCollector(ctx)
|
||
|
if err != nil {
|
||
|
t.Fatalf("err: %v", err)
|
||
|
}
|
||
|
|
||
|
if len(glv) != 1 {
|
||
|
t.Fatalf("Wrong number of gauges %v, expected %v", len(glv), 1)
|
||
|
}
|
||
|
|
||
|
if glv[0].Value != 1.0 {
|
||
|
t.Errorf("Entity count %v, expected %v", glv[0].Value, 1.0)
|
||
|
}
|
||
|
|
||
|
metricLabelsMatch(t, glv[0].Labels,
|
||
|
map[string]string{
|
||
|
"namespace": "root",
|
||
|
})
|
||
|
|
||
|
glv, err = core.entityGaugeCollectorByMount(ctx)
|
||
|
if err != nil {
|
||
|
t.Fatalf("err: %v", err)
|
||
|
}
|
||
|
|
||
|
if len(glv) != 2 {
|
||
|
t.Fatalf("Wrong number of gauges %v, expected %v", len(glv), 1)
|
||
|
}
|
||
|
|
||
|
if glv[0].Value != 1.0 {
|
||
|
t.Errorf("Alias count %v, expected %v", glv[0].Value, 1.0)
|
||
|
}
|
||
|
|
||
|
if glv[1].Value != 1.0 {
|
||
|
t.Errorf("Alias count %v, expected %v", glv[0].Value, 1.0)
|
||
|
}
|
||
|
|
||
|
// Sort both metrics.Label slices by Name, causing the Label
|
||
|
// with Name auth_method to be first in both arrays
|
||
|
sort.Slice(glv[0].Labels, func(i, j int) bool { return glv[0].Labels[i].Name < glv[0].Labels[j].Name })
|
||
|
sort.Slice(glv[1].Labels, func(i, j int) bool { return glv[1].Labels[i].Name < glv[1].Labels[j].Name })
|
||
|
|
||
|
// Sort the GaugeLabelValues slice by the Value of the first metric,
|
||
|
// in this case auth_method, in each metrics.Label slice
|
||
|
sort.Slice(glv, func(i, j int) bool { return glv[i].Labels[0].Value < glv[j].Labels[0].Value })
|
||
|
|
||
|
metricLabelsMatch(t, glv[0].Labels,
|
||
|
map[string]string{
|
||
|
"namespace": "root",
|
||
|
"auth_method": "github",
|
||
|
"mount_point": "auth/github/",
|
||
|
})
|
||
|
|
||
|
metricLabelsMatch(t, glv[1].Labels,
|
||
|
map[string]string{
|
||
|
"namespace": "root",
|
||
|
"auth_method": "userpass",
|
||
|
"mount_point": "auth/userpass/",
|
||
|
})
|
||
|
}
|