You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/bsr/bsr_test.go

720 lines
20 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package bsr_test
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/boundary/internal/bsr"
"github.com/hashicorp/boundary/internal/bsr/internal/fstest"
"github.com/hashicorp/boundary/internal/bsr/kms"
"github.com/hashicorp/boundary/internal/storage"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
)
func assertContainer(ctx context.Context, t *testing.T, path, state string, typ string, fs *fstest.MemContainer, keys *kms.Keys) {
t.Helper()
td := filepath.Join("testdata", t.Name(), state, path)
// meta
wantMeta, err := os.ReadFile(filepath.Join(td, string(typ)+"-recording.meta"))
require.NoError(t, err, "unable to find test data file")
meta, ok := fs.Files[string(typ)+"-recording.meta"]
require.True(t, ok, "container is missing meta file")
wantChecksumsRegex, err := regexp.Compile(string(wantMeta))
require.NoError(t, err)
assert.True(t, wantChecksumsRegex.MatchString(meta.Buf.String()))
// summary
wantSummary, err := os.ReadFile(filepath.Join(td, string(typ)+"-recording-summary.json"))
require.NoError(t, err, "unable to find test data file")
summary, ok := fs.Files[string(typ)+"-recording-summary.json"]
require.True(t, ok, "container is missing summary file")
assert.Equal(t, string(wantSummary), summary.Buf.String())
// SHA256SUM checksums
wantChecksums, err := os.ReadFile(filepath.Join(td, "SHA256SUM"))
require.NoError(t, err, "unable to find test data file")
checksums, ok := fs.Files["SHA256SUM"]
require.True(t, ok, "container is missing checksums file")
checksumSlice := strings.Split(string(wantChecksums), "\n")
var wChecksumsRegex []*regexp.Regexp
for _, c := range checksumSlice {
r, err := regexp.Compile(c)
require.NoError(t, err)
wChecksumsRegex = append(wChecksumsRegex, r)
}
for _, cr := range wChecksumsRegex {
assert.True(t, cr.MatchString(checksums.Buf.String()), "got:\n%s\nmust match:\n%s", checksums.Buf.String(), cr.String())
}
// SHA256SUM.sig signature file
sig, ok := fs.Files["SHA256SUM.sig"]
require.True(t, ok, "container is missing sig file")
switch state {
case "closed":
want, err := keys.SignWithPrivKey(ctx, checksums.Buf.Bytes())
require.NoError(t, err)
got := &wrapping.SigInfo{}
err = proto.Unmarshal(sig.Buf.Bytes(), got)
require.NoError(t, err)
assert.Empty(t,
cmp.Diff(
want,
got,
cmpopts.IgnoreUnexported(wrapping.SigInfo{}, wrapping.KeyInfo{}),
),
)
default:
assert.Equal(t, "", sig.Buf.String())
}
// journal
wantJournal, err := os.ReadFile(filepath.Join(td, ".journal"))
require.NoError(t, err, "unable to find test data file")
journal, ok := fs.Files[".journal"]
require.True(t, ok, "container is missing journal file")
journalSlice := strings.Split(string(wantJournal), "\n")
var wJournalRegex []*regexp.Regexp
for _, c := range journalSlice {
r, err := regexp.Compile(c)
require.NoError(t, err)
wJournalRegex = append(wJournalRegex, r)
}
for _, cr := range wJournalRegex {
assert.True(t, cr.MatchString(journal.Buf.String()), "got:\n%s\nmust match:\n%s", journal.Buf.String(), cr.String())
}
if typ == "session" {
// BSR keys, if this is a session container
bsrPub, ok := fs.Files["bsrKey.pub"]
require.True(t, ok, "container is missing bsrPub file")
assert.NotEmpty(t, bsrPub.Buf.String())
wrappedBsrKey, ok := fs.Files["wrappedBsrKey"]
require.True(t, ok, "container is missing wrappedBsrKey file")
assert.NotEmpty(t, wrappedBsrKey.Buf.String())
wrappedPrivKey, ok := fs.Files["wrappedPrivKey"]
require.True(t, ok, "container is missing wrappedPrivKey file")
assert.NotEmpty(t, wrappedPrivKey.Buf.String())
pubKeyBsrSignature, ok := fs.Files["pubKeyBsrSignature.sign"]
require.True(t, ok, "container is missing pubKeyBsrSignature.sign file")
assert.NotEmpty(t, pubKeyBsrSignature.Buf.String())
pubKeySelfSignature, ok := fs.Files["pubKeySelfSignature.sign"]
require.True(t, ok, "container is missing pubKeySelfSignature.sign file")
assert.NotEmpty(t, pubKeySelfSignature.Buf.String())
sessionMeta, ok := fs.Files["session-meta.json"]
require.True(t, ok, "container is missing session-meta.json file")
assert.NotEmpty(t, sessionMeta.Buf.String())
sm := &bsr.SessionMeta{}
err = json.Unmarshal(sessionMeta.Buf.Bytes(), sm)
require.NoError(t, err)
assert.Equal(t, sm, bsr.TestSessionMeta(strings.ReplaceAll(fs.Name, ".bsr", "")))
}
}
type connection struct {
mem *fstest.MemContainer
conn *bsr.Connection
id string
channels []*channel
files []*file
}
type channel struct {
mem *fstest.MemContainer
channel *bsr.Channel
id string
files []*file
}
type file struct {
mem *fstest.MemFile
file io.Writer
}
type createConn struct {
id string
channels []createChannel
files []createFile
}
type createChannel struct {
id string
files []createFile
}
type createFile struct {
typ string
dir bsr.Direction
}
func TestBsr(t *testing.T) {
ctx := context.Background()
cases := []struct {
name string
id string
opts []bsr.Option
c *fstest.MemFS
keys *kms.Keys
conns []createConn
}{
{
"session_not_multiplexed",
"session_123456789",
[]bsr.Option{},
fstest.NewMemFS(),
func() *kms.Keys {
keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session_123456789")
require.NoError(t, err)
return keys
}(),
[]createConn{
{
"conn_1",
nil,
[]createFile{
{"messages", bsr.Inbound},
{"messages", bsr.Outbound},
{"requests", bsr.Inbound},
{"requests", bsr.Outbound},
},
},
},
},
{
"session_multiplexed",
"session_123456789",
[]bsr.Option{bsr.WithSupportsMultiplex(true)},
fstest.NewMemFS(),
func() *kms.Keys {
keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session_123456789")
require.NoError(t, err)
return keys
}(),
[]createConn{
{
"conn_1",
[]createChannel{
{
"chan_1",
[]createFile{
{"messages", bsr.Inbound},
{"requests", bsr.Inbound},
{"messages", bsr.Outbound},
{"requests", bsr.Outbound},
},
},
},
[]createFile{
{"requests", bsr.Inbound},
{"requests", bsr.Outbound},
},
},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
srm := &bsr.SessionRecordingMeta{
Id: tc.id,
Protocol: bsr.Protocol("TEST"),
}
sessionMeta := bsr.TestSessionMeta(tc.id)
s, err := bsr.NewSession(ctx, srm, sessionMeta, tc.c, tc.keys, tc.opts...)
require.NoError(t, err)
require.NotNil(t, s)
sContainer, ok := tc.c.Containers[tc.id+".bsr"]
require.True(t, ok)
assertContainer(ctx, t, "", "opened", "session", sContainer, tc.keys)
createdConnections := make([]*connection, 0)
// create all the things
for _, conn := range tc.conns {
c, err := s.NewConnection(ctx, &bsr.ConnectionRecordingMeta{Id: conn.id})
require.NoError(t, err)
require.NotNil(t, c)
cContainer, ok := sContainer.Sub[conn.id+".connection"]
require.True(t, ok)
assertContainer(ctx, t, conn.id, "opened", "connection", cContainer, tc.keys)
ff := make([]*file, 0, len(conn.files))
for _, f := range conn.files {
var w io.Writer
var err error
switch f.typ {
case "messages":
w, err = c.NewMessagesWriter(ctx, f.dir)
case "requests":
w, err = c.NewRequestsWriter(ctx, f.dir)
}
require.NoError(t, err)
fname := fmt.Sprintf("%s-%s.data", f.typ, f.dir.String())
memf, ok := cContainer.Files[fname]
require.True(t, ok, "file %s not in container %s", fname, cContainer.Name)
require.NoError(t, err)
ff = append(ff, &file{
mem: memf,
file: w,
})
}
createdChannels := make([]*channel, 0, len(conn.channels))
for _, chann := range conn.channels {
ch, err := c.NewChannel(ctx, &bsr.ChannelRecordingMeta{Id: chann.id, Type: "chan"})
require.NoError(t, err)
require.NotNil(t, ch)
chContainer, ok := cContainer.Sub[chann.id+".channel"]
require.True(t, ok)
assertContainer(ctx, t, filepath.Join(conn.id, chann.id), "opened", "channel", chContainer, tc.keys)
ff := make([]*file, 0, len(chann.files))
for _, f := range chann.files {
var w io.Writer
var err error
switch f.typ {
case "messages":
w, err = ch.NewMessagesWriter(ctx, f.dir)
case "requests":
w, err = ch.NewRequestsWriter(ctx, f.dir)
}
require.NoError(t, err)
fname := fmt.Sprintf("%s-%s.data", f.typ, f.dir.String())
memf, ok := chContainer.Files[fname]
require.True(t, ok, "file %s not in container %s", fname, chContainer.Name)
require.NoError(t, err)
ff = append(ff, &file{
mem: memf,
file: w,
})
}
createdChannels = append(createdChannels, &channel{
mem: chContainer,
channel: ch,
id: chann.id,
files: ff,
})
}
createdConnections = append(createdConnections, &connection{
mem: cContainer,
conn: c,
id: conn.id,
channels: createdChannels,
files: ff,
})
}
// now close all the things that where created.
for _, conn := range createdConnections {
for _, channel := range conn.channels {
for _, f := range channel.files {
v, ok := f.file.(io.Closer)
require.True(t, ok, "file is not a io.Closer")
err = v.Close()
require.NoError(t, err)
}
err = channel.channel.Close(ctx)
require.NoError(t, err)
assertContainer(ctx, t, filepath.Join(conn.id, channel.id), "closed", "channel", channel.mem, tc.keys)
}
for _, f := range conn.files {
v, ok := f.file.(io.Closer)
require.True(t, ok, "file is not a io.Closer")
err = v.Close()
require.NoError(t, err)
}
err = conn.conn.Close(ctx)
require.NoError(t, err)
assertContainer(ctx, t, conn.id, "closed", "connection", conn.mem, tc.keys)
}
err = s.Close(ctx)
require.NoError(t, err)
assertContainer(ctx, t, "", "closed", "session", sContainer, tc.keys)
})
}
}
func TestNewSessionErrors(t *testing.T) {
ctx := context.Background()
keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session")
require.NoError(t, err)
cases := []struct {
name string
meta *bsr.SessionRecordingMeta
sessionMeta *bsr.SessionMeta
f storage.FS
keys *kms.Keys
wantError error
}{
{
"nil-meta",
nil,
bsr.TestSessionMeta("session"),
&fstest.MemFS{},
keys,
errors.New("bsr.NewSession: missing meta: invalid parameter"),
},
{
"nil-session-meta",
bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")),
nil,
&fstest.MemFS{},
keys,
errors.New("bsr.NewSession: missing session meta: invalid parameter"),
},
{
"empty-session-id",
bsr.TestSessionRecordingMeta("", bsr.Protocol("TEST")),
bsr.TestSessionMeta("session"),
&fstest.MemFS{},
keys,
errors.New("bsr.NewSession: missing session id: invalid parameter"),
},
{
"nil-fs",
bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")),
bsr.TestSessionMeta("session"),
nil,
keys,
errors.New("bsr.NewSession: missing storage fs: invalid parameter"),
},
{
"nil-keys",
bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")),
bsr.TestSessionMeta("session"),
&fstest.MemFS{},
nil,
errors.New("bsr.NewSession: missing kms keys: invalid parameter"),
},
{
"missing-bsr-signature",
bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")),
bsr.TestSessionMeta("session"),
&fstest.MemFS{},
&kms.Keys{
PubKey: keys.PubKey,
WrappedBsrKey: keys.WrappedBsrKey,
WrappedPrivKey: keys.WrappedPrivKey,
PubKeySelfSignature: keys.PubKeySelfSignature,
},
errors.New("bsr.persistBsrSessionKeys: missing kms pub key BSR signature: invalid parameter\nbsr.NewSession: could not persist BSR keys"),
},
{
"missing-pub-signature",
bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")),
bsr.TestSessionMeta("session"),
&fstest.MemFS{},
&kms.Keys{
PubKey: keys.PubKey,
WrappedBsrKey: keys.WrappedBsrKey,
WrappedPrivKey: keys.WrappedPrivKey,
PubKeyBsrSignature: keys.PubKeyBsrSignature,
},
errors.New("bsr.persistBsrSessionKeys: missing kms pub key self signature: invalid parameter\nbsr.NewSession: could not persist BSR keys"),
},
{
"missing-pub-key",
bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")),
bsr.TestSessionMeta("session"),
&fstest.MemFS{},
&kms.Keys{
WrappedBsrKey: keys.WrappedBsrKey,
WrappedPrivKey: keys.WrappedPrivKey,
PubKeySelfSignature: keys.PubKeySelfSignature,
PubKeyBsrSignature: keys.PubKeyBsrSignature,
},
errors.New("bsr.persistBsrSessionKeys: missing kms pub key: invalid parameter\nbsr.NewSession: could not persist BSR keys"),
},
{
"missing-wrapped-bsr-key",
bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")),
bsr.TestSessionMeta("session"),
&fstest.MemFS{},
&kms.Keys{
PubKey: keys.PubKey,
WrappedPrivKey: keys.WrappedPrivKey,
PubKeySelfSignature: keys.PubKeySelfSignature,
PubKeyBsrSignature: keys.PubKeyBsrSignature,
},
errors.New("bsr.persistBsrSessionKeys: missing kms wrapped BSR key: invalid parameter\nbsr.NewSession: could not persist BSR keys"),
},
{
"missing-wrapped-priv-key",
bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")),
bsr.TestSessionMeta("session"),
&fstest.MemFS{},
&kms.Keys{
PubKey: keys.PubKey,
WrappedBsrKey: keys.WrappedBsrKey,
PubKeySelfSignature: keys.PubKeySelfSignature,
PubKeyBsrSignature: keys.PubKeyBsrSignature,
},
errors.New("bsr.persistBsrSessionKeys: missing kms wrapped priv key: invalid parameter\nbsr.NewSession: could not persist BSR keys"),
},
{
"fs-new-error",
bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")),
bsr.TestSessionMeta("session"),
fstest.NewMemFS(fstest.WithNewFunc(func(_ context.Context, _ string) (storage.Container, error) {
return nil, errors.New("fs new error")
})),
keys,
errors.New("fs new error"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := bsr.NewSession(ctx, tc.meta, tc.sessionMeta, tc.f, tc.keys)
require.Error(t, err)
assert.EqualError(t, err, tc.wantError.Error())
})
}
}
func TestNewConnectionErrors(t *testing.T) {
ctx := context.Background()
keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session")
require.NoError(t, err)
cases := []struct {
name string
session *bsr.Session
meta *bsr.ConnectionRecordingMeta
wantError error
}{
{
"nil-meta",
func() *bsr.Session {
s, err := bsr.NewSession(ctx, bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")), bsr.TestSessionMeta("session"), &fstest.MemFS{}, keys)
require.NoError(t, err)
return s
}(),
nil,
errors.New("bsr.(Session).NewConnection: missing connection meta: invalid parameter"),
},
{
"empty-connection-id",
func() *bsr.Session {
s, err := bsr.NewSession(ctx, bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST")), bsr.TestSessionMeta("session"), &fstest.MemFS{}, keys)
require.NoError(t, err)
return s
}(),
&bsr.ConnectionRecordingMeta{Id: ""},
errors.New("bsr.(Session).NewConnection: missing connection id: invalid parameter"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := tc.session.NewConnection(ctx, tc.meta)
require.Error(t, err)
assert.EqualError(t, err, tc.wantError.Error())
})
}
}
func TestNewChannelErrors(t *testing.T) {
ctx := context.Background()
keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session")
require.NoError(t, err)
srm := bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST"))
sessionMeta := bsr.TestSessionMeta("session")
cases := []struct {
name string
connection *bsr.Connection
meta *bsr.ChannelRecordingMeta
wantError error
}{
{
"nil-meta",
func() *bsr.Connection {
s, err := bsr.NewSession(ctx, srm, sessionMeta, &fstest.MemFS{}, keys, bsr.WithSupportsMultiplex(true))
require.NoError(t, err)
c, err := s.NewConnection(ctx, &bsr.ConnectionRecordingMeta{Id: "connection"})
require.NoError(t, err)
return c
}(),
nil,
errors.New("bsr.(Connection).NewChannel: missing channel meta: invalid parameter"),
},
{
"empty-connection-id",
func() *bsr.Connection {
s, err := bsr.NewSession(ctx, srm, sessionMeta, &fstest.MemFS{}, keys, bsr.WithSupportsMultiplex(true))
require.NoError(t, err)
c, err := s.NewConnection(ctx, &bsr.ConnectionRecordingMeta{Id: "connection"})
require.NoError(t, err)
return c
}(),
&bsr.ChannelRecordingMeta{Id: ""},
errors.New("bsr.(Connection).NewChannel: missing channel id: invalid parameter"),
},
{
"not-multiplexed",
func() *bsr.Connection {
s, err := bsr.NewSession(ctx, srm, sessionMeta, &fstest.MemFS{}, keys, bsr.WithSupportsMultiplex(false))
require.NoError(t, err)
c, err := s.NewConnection(ctx, &bsr.ConnectionRecordingMeta{Id: "connection"})
require.NoError(t, err)
return c
}(),
&bsr.ChannelRecordingMeta{Id: ""},
errors.New("bsr.(Connection).NewChannel: connection cannot make channels: not supported by protocol"),
},
{
"not-multiplexed-default",
func() *bsr.Connection {
s, err := bsr.NewSession(ctx, srm, sessionMeta, &fstest.MemFS{}, keys)
require.NoError(t, err)
c, err := s.NewConnection(ctx, &bsr.ConnectionRecordingMeta{Id: "connection"})
require.NoError(t, err)
return c
}(),
&bsr.ChannelRecordingMeta{Id: ""},
errors.New("bsr.(Connection).NewChannel: connection cannot make channels: not supported by protocol"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := tc.connection.NewChannel(ctx, tc.meta)
require.Error(t, err)
assert.EqualError(t, err, tc.wantError.Error())
})
}
}
func TestChannelNewMessagesWriterErrors(t *testing.T) {
ctx := context.Background()
keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session")
require.NoError(t, err)
srm := bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST"))
sessionMeta := bsr.TestSessionMeta("session")
cases := []struct {
name string
channel *bsr.Channel
dir bsr.Direction
wantError error
}{
{
"invalid-dir",
func() *bsr.Channel {
s, err := bsr.NewSession(ctx, srm, sessionMeta, &fstest.MemFS{}, keys, bsr.WithSupportsMultiplex(true))
require.NoError(t, err)
c, err := s.NewConnection(ctx, &bsr.ConnectionRecordingMeta{Id: "connection"})
require.NoError(t, err)
ch, err := c.NewChannel(ctx, &bsr.ChannelRecordingMeta{Id: "channel"})
require.NoError(t, err)
return ch
}(),
bsr.Direction(uint8(255)),
errors.New("bsr.(Channel).NewMessagesWriter: invalid direction: invalid parameter"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := tc.channel.NewMessagesWriter(ctx, tc.dir)
require.Error(t, err)
assert.EqualError(t, err, tc.wantError.Error())
})
}
}
func TestChannelNewRequestsWriterErrors(t *testing.T) {
ctx := context.Background()
keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), "session")
require.NoError(t, err)
srm := bsr.TestSessionRecordingMeta("session_recording", bsr.Protocol("TEST"))
sessionMeta := bsr.TestSessionMeta("session")
cases := []struct {
name string
channel *bsr.Channel
dir bsr.Direction
wantError error
}{
{
"invalid-dir",
func() *bsr.Channel {
s, err := bsr.NewSession(ctx, srm, sessionMeta, &fstest.MemFS{}, keys, bsr.WithSupportsMultiplex(true))
require.NoError(t, err)
c, err := s.NewConnection(ctx, &bsr.ConnectionRecordingMeta{Id: "connection"})
require.NoError(t, err)
ch, err := c.NewChannel(ctx, &bsr.ChannelRecordingMeta{Id: "channel"})
require.NoError(t, err)
return ch
}(),
bsr.Direction(uint8(255)),
errors.New("bsr.(Channel).NewRequestsWriter: invalid direction: invalid parameter"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := tc.channel.NewRequestsWriter(ctx, tc.dir)
require.Error(t, err)
assert.EqualError(t, err, tc.wantError.Error())
})
}
}