open-vault/vault/audit_test.go

616 lines
15 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package vault
import (
"context"
"errors"
"fmt"
"reflect"
"strings"
"testing"
"time"
"github.com/hashicorp/vault/helper/testhelpers/corehelpers"
"github.com/hashicorp/errwrap"
log "github.com/hashicorp/go-hclog"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/audit"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/logging"
"github.com/hashicorp/vault/sdk/logical"
"github.com/mitchellh/copystructure"
)
func TestAudit_ReadOnlyViewDuringMount(t *testing.T) {
c, _, _ := TestCoreUnsealed(t)
c.auditBackends["noop"] = func(ctx context.Context, config *audit.BackendConfig) (audit.Backend, error) {
err := config.SaltView.Put(ctx, &logical.StorageEntry{
Key: "bar",
Value: []byte("baz"),
})
if err == nil || !strings.Contains(err.Error(), logical.ErrSetupReadOnly.Error()) {
t.Fatalf("expected a read-only error")
}
factory := corehelpers.NoopAuditFactory(nil)
return factory(ctx, config)
}
me := &MountEntry{
Table: auditTableType,
Path: "foo",
Type: "noop",
}
err := c.enableAudit(namespace.RootContext(nil), me, true)
if err != nil {
t.Fatalf("err: %v", err)
}
}
func TestCore_EnableAudit(t *testing.T) {
c, keys, _ := TestCoreUnsealed(t)
c.auditBackends["noop"] = corehelpers.NoopAuditFactory(nil)
me := &MountEntry{
Table: auditTableType,
Path: "foo",
Type: "noop",
}
err := c.enableAudit(namespace.RootContext(nil), me, true)
if err != nil {
t.Fatalf("err: %v", err)
}
if !c.auditBroker.IsRegistered("foo/") {
t.Fatalf("missing audit backend")
}
conf := &CoreConfig{
Physical: c.physical,
AuditBackends: make(map[string]audit.Factory),
DisableMlock: true,
}
conf.AuditBackends["noop"] = corehelpers.NoopAuditFactory(nil)
c2, err := NewCore(conf)
if err != nil {
t.Fatalf("err: %v", err)
}
defer c2.Shutdown()
for i, key := range keys {
unseal, err := TestCoreUnseal(c2, key)
if err != nil {
t.Fatalf("err: %v", err)
}
if i+1 == len(keys) && !unseal {
t.Fatalf("should be unsealed")
}
}
// Verify matching audit tables
if !reflect.DeepEqual(c.audit, c2.audit) {
t.Fatalf("mismatch: %v %v", c.audit, c2.audit)
}
// Check for registration
if !c2.auditBroker.IsRegistered("foo/") {
t.Fatalf("missing audit backend")
}
}
func TestCore_EnableAudit_MixedFailures(t *testing.T) {
c, _, _ := TestCoreUnsealed(t)
c.auditBackends["noop"] = corehelpers.NoopAuditFactory(nil)
c.auditBackends["fail"] = func(ctx context.Context, config *audit.BackendConfig) (audit.Backend, error) {
return nil, fmt.Errorf("failing enabling")
}
c.audit = &MountTable{
Type: auditTableType,
Entries: []*MountEntry{
{
Table: auditTableType,
Path: "noop/",
Type: "noop",
UUID: "abcd",
},
{
Table: auditTableType,
Path: "noop2/",
Type: "noop",
UUID: "bcde",
},
},
}
// Both should set up successfully
err := c.setupAudits(context.Background())
if err != nil {
t.Fatal(err)
}
// We expect this to work because the other entry is still valid
c.audit.Entries[0].Type = "fail"
err = c.setupAudits(context.Background())
if err != nil {
t.Fatal(err)
}
// No audit backend set up successfully, so expect error
c.audit.Entries[1].Type = "fail"
err = c.setupAudits(context.Background())
if err == nil {
t.Fatal("expected error")
}
}
// Test that the local table actually gets populated as expected with local
// entries, and that upon reading the entries from both are recombined
// correctly
func TestCore_EnableAudit_Local(t *testing.T) {
c, _, _ := TestCoreUnsealed(t)
c.auditBackends["noop"] = corehelpers.NoopAuditFactory(nil)
c.auditBackends["fail"] = func(ctx context.Context, config *audit.BackendConfig) (audit.Backend, error) {
return nil, fmt.Errorf("failing enabling")
}
c.audit = &MountTable{
Type: auditTableType,
Entries: []*MountEntry{
{
Table: auditTableType,
Path: "noop/",
Type: "noop",
UUID: "abcd",
Accessor: "noop-abcd",
NamespaceID: namespace.RootNamespaceID,
namespace: namespace.RootNamespace,
},
{
Table: auditTableType,
Path: "noop2/",
Type: "noop",
UUID: "bcde",
Accessor: "noop-bcde",
NamespaceID: namespace.RootNamespaceID,
namespace: namespace.RootNamespace,
},
},
}
// Both should set up successfully
err := c.setupAudits(context.Background())
if err != nil {
t.Fatal(err)
}
rawLocal, err := c.barrier.Get(context.Background(), coreLocalAuditConfigPath)
if err != nil {
t.Fatal(err)
}
if rawLocal == nil {
t.Fatal("expected non-nil local audit")
}
localAuditTable := &MountTable{}
if err := jsonutil.DecodeJSON(rawLocal.Value, localAuditTable); err != nil {
t.Fatal(err)
}
if len(localAuditTable.Entries) > 0 {
t.Fatalf("expected no entries in local audit table, got %#v", localAuditTable)
}
c.audit.Entries[1].Local = true
if err := c.persistAudit(context.Background(), c.audit, false); err != nil {
t.Fatal(err)
}
rawLocal, err = c.barrier.Get(context.Background(), coreLocalAuditConfigPath)
if err != nil {
t.Fatal(err)
}
if rawLocal == nil {
t.Fatal("expected non-nil local audit")
}
localAuditTable = &MountTable{}
if err := jsonutil.DecodeJSON(rawLocal.Value, localAuditTable); err != nil {
t.Fatal(err)
}
if len(localAuditTable.Entries) != 1 {
t.Fatalf("expected one entry in local audit table, got %#v", localAuditTable)
}
oldAudit := c.audit
if err := c.loadAudits(context.Background()); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(oldAudit, c.audit) {
t.Fatalf("expected\n%#v\ngot\n%#v\n", oldAudit, c.audit)
}
if len(c.audit.Entries) != 2 {
t.Fatalf("expected two audit entries, got %#v", localAuditTable)
}
}
func TestCore_DisableAudit(t *testing.T) {
c, keys, _ := TestCoreUnsealed(t)
c.auditBackends["noop"] = corehelpers.NoopAuditFactory(nil)
existed, err := c.disableAudit(namespace.RootContext(nil), "foo", true)
if existed && err != nil {
t.Fatalf("existed: %v; err: %v", existed, err)
}
me := &MountEntry{
Table: auditTableType,
Path: "foo",
Type: "noop",
}
err = c.enableAudit(namespace.RootContext(nil), me, true)
if err != nil {
t.Fatalf("err: %v", err)
}
existed, err = c.disableAudit(namespace.RootContext(nil), "foo", true)
if !existed || err != nil {
t.Fatalf("existed: %v; err: %v", existed, err)
}
// Check for registration
if c.auditBroker.IsRegistered("foo") {
t.Fatalf("audit backend present")
}
conf := &CoreConfig{
Physical: c.physical,
DisableMlock: true,
}
c2, err := NewCore(conf)
if err != nil {
t.Fatalf("err: %v", err)
}
defer c2.Shutdown()
for i, key := range keys {
unseal, err := TestCoreUnseal(c2, key)
if err != nil {
t.Fatalf("err: %v", err)
}
if i+1 == len(keys) && !unseal {
t.Fatalf("should be unsealed")
}
}
// Verify matching mount tables
if !reflect.DeepEqual(c.audit, c2.audit) {
t.Fatalf("mismatch:\n%#v\n%#v", c.audit, c2.audit)
}
}
func TestCore_DefaultAuditTable(t *testing.T) {
c, keys, _ := TestCoreUnsealed(t)
verifyDefaultAuditTable(t, c.audit)
// Verify we have an audit broker
if c.auditBroker == nil {
t.Fatalf("missing audit broker")
}
// Start a second core with same physical
conf := &CoreConfig{
Physical: c.physical,
DisableMlock: true,
}
c2, err := NewCore(conf)
if err != nil {
t.Fatalf("err: %v", err)
}
defer c2.Shutdown()
for i, key := range keys {
unseal, err := TestCoreUnseal(c2, key)
if err != nil {
t.Fatalf("err: %v", err)
}
if i+1 == len(keys) && !unseal {
t.Fatalf("should be unsealed")
}
}
// Verify matching mount tables
if !reflect.DeepEqual(c.audit, c2.audit) {
t.Fatalf("mismatch: %v %v", c.audit, c2.audit)
}
}
func TestDefaultAuditTable(t *testing.T) {
table := defaultAuditTable()
verifyDefaultAuditTable(t, table)
}
func verifyDefaultAuditTable(t *testing.T, table *MountTable) {
if len(table.Entries) != 0 {
t.Fatalf("bad: %v", table.Entries)
}
if table.Type != auditTableType {
t.Fatalf("bad: %v", *table)
}
}
func TestAuditBroker_LogRequest(t *testing.T) {
l := logging.NewVaultLogger(log.Trace)
b := NewAuditBroker(l)
a1 := corehelpers.TestNoopAudit(t, nil)
a2 := corehelpers.TestNoopAudit(t, nil)
b.Register("foo", a1, nil, false)
b.Register("bar", a2, nil, false)
auth := &logical.Auth{
ClientToken: "foo",
Policies: []string{"dev", "ops"},
Metadata: map[string]string{
"user": "armon",
"source": "github",
},
}
req := &logical.Request{
Operation: logical.ReadOperation,
Path: "sys/mounts",
}
// Copy so we can verify nothing changed
authCopyRaw, err := copystructure.Copy(auth)
if err != nil {
t.Fatal(err)
}
authCopy := authCopyRaw.(*logical.Auth)
reqCopyRaw, err := copystructure.Copy(req)
if err != nil {
t.Fatal(err)
}
reqCopy := reqCopyRaw.(*logical.Request)
// Create an identifier for the request to verify against
req.ID, err = uuid.GenerateUUID()
if err != nil {
t.Fatalf("failed to generate identifier for the request: path%s err: %v", req.Path, err)
}
reqCopy.ID = req.ID
reqErrs := errors.New("errs")
headersConf := &AuditedHeadersConfig{
Headers: make(map[string]*auditedHeaderSettings),
}
logInput := &logical.LogInput{
Auth: authCopy,
Request: reqCopy,
OuterErr: reqErrs,
}
ctx := namespace.RootContext(context.Background())
err = b.LogRequest(ctx, logInput, headersConf)
if err != nil {
t.Fatalf("err: %v", err)
}
for _, a := range []*corehelpers.NoopAudit{a1, a2} {
if !reflect.DeepEqual(a.ReqAuth[0], auth) {
t.Fatalf("Bad: %#v", a.ReqAuth[0])
}
if !reflect.DeepEqual(a.Req[0], req) {
t.Fatalf("Bad: %#v\n wanted %#v", a.Req[0], req)
}
if !reflect.DeepEqual(a.ReqErrs[0], reqErrs) {
t.Fatalf("Bad: %#v", a.ReqErrs[0])
}
}
// Should still work with one failing backend
a1.ReqErr = fmt.Errorf("failed")
logInput = &logical.LogInput{
Auth: auth,
Request: req,
}
if err := b.LogRequest(ctx, logInput, headersConf); err != nil {
t.Fatalf("err: %v", err)
}
// Should FAIL work with both failing backends
a2.ReqErr = fmt.Errorf("failed")
if err := b.LogRequest(ctx, logInput, headersConf); !errwrap.Contains(err, "no audit backend succeeded in logging the request") {
t.Fatalf("err: %v", err)
}
}
func TestAuditBroker_LogResponse(t *testing.T) {
l := logging.NewVaultLogger(log.Trace)
b := NewAuditBroker(l)
a1 := corehelpers.TestNoopAudit(t, nil)
a2 := corehelpers.TestNoopAudit(t, nil)
b.Register("foo", a1, nil, false)
b.Register("bar", a2, nil, false)
auth := &logical.Auth{
NumUses: 10,
ClientToken: "foo",
Policies: []string{"dev", "ops"},
Metadata: map[string]string{
"user": "armon",
"source": "github",
},
}
req := &logical.Request{
Operation: logical.ReadOperation,
Path: "sys/mounts",
}
resp := &logical.Response{
Secret: &logical.Secret{
LeaseOptions: logical.LeaseOptions{
TTL: 1 * time.Hour,
},
},
Data: map[string]interface{}{
"user": "root",
"password": "password",
},
}
respErr := fmt.Errorf("permission denied")
// Copy so we can verify nothing changed
authCopyRaw, err := copystructure.Copy(auth)
if err != nil {
t.Fatal(err)
}
authCopy := authCopyRaw.(*logical.Auth)
reqCopyRaw, err := copystructure.Copy(req)
if err != nil {
t.Fatal(err)
}
reqCopy := reqCopyRaw.(*logical.Request)
respCopyRaw, err := copystructure.Copy(resp)
if err != nil {
t.Fatal(err)
}
respCopy := respCopyRaw.(*logical.Response)
headersConf := &AuditedHeadersConfig{
Headers: make(map[string]*auditedHeaderSettings),
}
logInput := &logical.LogInput{
Auth: authCopy,
Request: reqCopy,
Response: respCopy,
OuterErr: respErr,
}
ctx := namespace.RootContext(context.Background())
err = b.LogResponse(ctx, logInput, headersConf)
if err != nil {
t.Fatalf("err: %v", err)
}
for _, a := range []*corehelpers.NoopAudit{a1, a2} {
if !reflect.DeepEqual(a.RespAuth[0], auth) {
t.Fatalf("Bad: %#v", a.ReqAuth[0])
}
if !reflect.DeepEqual(a.RespReq[0], req) {
t.Fatalf("Bad: %#v", a.Req[0])
}
if !reflect.DeepEqual(a.Resp[0], resp) {
t.Fatalf("Bad: %#v", a.Resp[0])
}
if !reflect.DeepEqual(a.RespErrs[0], respErr) {
t.Fatalf("Expected\n%v\nGot\n%#v", respErr, a.RespErrs[0])
}
}
// Should still work with one failing backend
a1.RespErr = fmt.Errorf("failed")
logInput = &logical.LogInput{
Auth: auth,
Request: req,
Response: resp,
OuterErr: respErr,
}
err = b.LogResponse(ctx, logInput, headersConf)
if err != nil {
t.Fatalf("err: %v", err)
}
// Should FAIL work with both failing backends
a2.RespErr = fmt.Errorf("failed")
err = b.LogResponse(ctx, logInput, headersConf)
if !strings.Contains(err.Error(), "no audit backend succeeded in logging the response") {
t.Fatalf("err: %v", err)
}
}
func TestAuditBroker_AuditHeaders(t *testing.T) {
logger := logging.NewVaultLogger(log.Trace)
b := NewAuditBroker(logger)
_, barrier, _ := mockBarrier(t)
view := NewBarrierView(barrier, "headers/")
a1 := corehelpers.TestNoopAudit(t, nil)
a2 := corehelpers.TestNoopAudit(t, nil)
b.Register("foo", a1, nil, false)
b.Register("bar", a2, nil, false)
auth := &logical.Auth{
ClientToken: "foo",
Policies: []string{"dev", "ops"},
Metadata: map[string]string{
"user": "armon",
"source": "github",
},
}
req := &logical.Request{
Operation: logical.ReadOperation,
Path: "sys/mounts",
Headers: map[string][]string{
"X-Test-Header": {"foo"},
"X-Vault-Header": {"bar"},
"Content-Type": {"baz"},
},
}
respErr := fmt.Errorf("permission denied")
// Copy so we can verify nothing changed
reqCopyRaw, err := copystructure.Copy(req)
if err != nil {
t.Fatal(err)
}
reqCopy := reqCopyRaw.(*logical.Request)
headersConf := &AuditedHeadersConfig{
view: view,
}
headersConf.add(context.Background(), "X-Test-Header", false)
headersConf.add(context.Background(), "X-Vault-Header", false)
logInput := &logical.LogInput{
Auth: auth,
Request: reqCopy,
OuterErr: respErr,
}
ctx := namespace.RootContext(context.Background())
err = b.LogRequest(ctx, logInput, headersConf)
if err != nil {
t.Fatalf("err: %v", err)
}
expected := map[string][]string{
"x-test-header": {"foo"},
"x-vault-header": {"bar"},
}
for _, a := range []*corehelpers.NoopAudit{a1, a2} {
if !reflect.DeepEqual(a.ReqHeaders[0], expected) {
t.Fatalf("Bad audited headers: %#v", a.Req[0].Headers)
}
}
// Should still work with one failing backend
a1.ReqErr = fmt.Errorf("failed")
logInput = &logical.LogInput{
Auth: auth,
Request: req,
OuterErr: respErr,
}
err = b.LogRequest(ctx, logInput, headersConf)
if err != nil {
t.Fatalf("err: %v", err)
}
// Should FAIL work with both failing backends
a2.ReqErr = fmt.Errorf("failed")
err = b.LogRequest(ctx, logInput, headersConf)
if !errwrap.Contains(err, "no audit backend succeeded in logging the request") {
t.Fatalf("err: %v", err)
}
}