From 4564d1590185bba08299305dd0863d3caad75987 Mon Sep 17 00:00:00 2001 From: Elim Tsiagbey Date: Fri, 4 Aug 2023 14:31:20 -0400 Subject: [PATCH] feat: Validate BSR files (#3507) * feat: Validate BSR files Add support for validating all files in a Boundary Session Recording (BSR). All files and sub container files will be verified by comparing it against checksums for each file in `SHA256SUM` file ### Considerations - Validation will continue even if there's an error encountered during validation. The validation error will be added to the Validation struct `Error` field --- internal/bsr/bsr.go | 159 ++++++++ internal/bsr/bsr_validate_test.go | 621 ++++++++++++++++++++++++++++++ 2 files changed, 780 insertions(+) create mode 100644 internal/bsr/bsr_validate_test.go diff --git a/internal/bsr/bsr.go b/internal/bsr/bsr.go index 307816842..63be70c31 100644 --- a/internal/bsr/bsr.go +++ b/internal/bsr/bsr.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "strings" "github.com/hashicorp/boundary/internal/bsr/internal/checksum" "github.com/hashicorp/boundary/internal/bsr/internal/is" @@ -244,6 +245,164 @@ func OpenSession(ctx context.Context, sessionRecordingId string, f storage.FS, k return session, nil } +// Validation provides the results from validating a bsr. +type Validation struct { + SessionRecordingId string + Valid bool + SessionRecordingValidation *ContainerValidation +} + +// ContainerValidation contains the results from validating a container in a bsr. +type ContainerValidation struct { + Name string + ContainerType ContainerType + Error error + FileChecksumValidations ContainerChecksumValidation + SubContainers []*ContainerValidation +} + +// Validate retrieves a BSR from storage using the sessionRecordingId and validates the BSR. +// All files and sub container files will be verified by comparing it against checksums for +// each file in SHA256SUM file. +// +// Validation will continue even if there's an error encountered during validation. +// The validation error will be added to the ContainerValidation struct "Error" field for that container. +func Validate(ctx context.Context, sessionRecordingId string, f storage.FS, keyUnwrapFn kms.KeyUnwrapCallbackFunc) (v *Validation, err error) { + const op = "bsr.Validate" + + switch { + case sessionRecordingId == "": + return nil, fmt.Errorf("%s: missing session recording id: %w", op, ErrInvalidParameter) + case f == nil: + return nil, fmt.Errorf("%s: missing storage: %w", op, ErrInvalidParameter) + case keyUnwrapFn == nil: + return nil, fmt.Errorf("%s: missing key unwrap function: %w", op, ErrInvalidParameter) + } + + session, err := OpenSession(ctx, sessionRecordingId, f, keyUnwrapFn) + if err != nil { + return nil, fmt.Errorf("%s: failed to retrieve session for %s: %w", op, sessionRecordingId, err) + } + defer func() { + if closeErr := session.Close(ctx); closeErr != nil { + err = errors.Join(err, fmt.Errorf("%s: %w", op, closeErr)) + } + }() + + validation := &Validation{ + SessionRecordingId: sessionRecordingId, + Valid: true, + } + + sessionContainerValidation := validateContainer(ctx, validation, SessionContainer, session.container, session.Meta.Id) + validation.SessionRecordingValidation = sessionContainerValidation + + // Validate all connections under session + for connId := range session.Meta.connections { + // Get connection id + connKey := connId + lastDotIndex := strings.LastIndex(connId, ".connection") + if lastDotIndex == -1 { + validation.Valid = false + sessionContainerValidation.SubContainers = append(sessionContainerValidation.SubContainers, &ContainerValidation{ + Name: connId, + ContainerType: ConnectionContainer, + Error: fmt.Errorf("%s: malformed BSR for: %s", op, connId), + }) + continue + } + + connKey = connId[:lastDotIndex] + + // Open current connection + connection, err := session.OpenConnection(ctx, connKey) + if err != nil { + validation.Valid = false + sessionContainerValidation.SubContainers = append(sessionContainerValidation.SubContainers, &ContainerValidation{ + Name: connId, + ContainerType: ConnectionContainer, + Error: fmt.Errorf("%s: failed to retrieve connection for %s: %w", op, connId, err), + }) + continue + } + + connectionContainerValidation := validateContainer(ctx, validation, ConnectionContainer, connection.container, connection.Meta.Id) + sessionContainerValidation.SubContainers = append(sessionContainerValidation.SubContainers, connectionContainerValidation) + + // Validate all channels under current connection + for chId := range connection.Meta.channels { + // Get channel id + chKey := chId + lastDotIndex := strings.LastIndex(chId, ".channel") + if lastDotIndex == -1 { + validation.Valid = false + sessionContainerValidation.SubContainers = append(connectionContainerValidation.SubContainers, &ContainerValidation{ + Name: chId, + ContainerType: ChannelContainer, + Error: fmt.Errorf("%s: malformed BSR for: %s", op, chId), + }) + continue + } + + chKey = chId[:lastDotIndex] + + // Open current connection + channel, err := connection.OpenChannel(ctx, chKey) + if err != nil { + validation.Valid = false + sessionContainerValidation.SubContainers = append(connectionContainerValidation.SubContainers, &ContainerValidation{ + Name: chId, + ContainerType: ChannelContainer, + Error: fmt.Errorf("%s: failed to retrieve channel for %s: %w", op, chId, err), + }) + continue + } + + channelContainerValidation := validateContainer(ctx, validation, ChannelContainer, channel.container, channel.Meta.Id) + err = channel.Close(ctx) + if err != nil { + channelContainerValidation.Error = errors.Join(channelContainerValidation.Error, fmt.Errorf("%s: failed to close channel for %s: %w", op, chId, err)) + } + connectionContainerValidation.SubContainers = append(connectionContainerValidation.SubContainers, channelContainerValidation) + } + + err = connection.Close(ctx) + if err != nil { + connectionContainerValidation.Error = errors.Join(connectionContainerValidation.Error, fmt.Errorf("%s: failed to close connection for %s: %w", op, connId, err)) + } + } + + return validation, nil +} + +// ValidateContainer validates the checksums of all files in a container +func validateContainer(ctx context.Context, v *Validation, ct ContainerType, c *container, name string) *ContainerValidation { + const op = "bsr.(Validate).ValidateContainer" + + containerValidation := &ContainerValidation{ + Name: name, + ContainerType: ct, + } + + containerChecksumValidation, err := c.ValidateChecksums(ctx) + if err != nil { + v.Valid = false + containerValidation.Error = fmt.Errorf("%s: failed to validate %s: %w", op, name, err) + return containerValidation + } + + containerValidation.FileChecksumValidations = containerChecksumValidation + + failedChecksums := containerValidation.FileChecksumValidations.GetFailedItems() + + // Update validation field only if there are failed checksums + if len(failedChecksums) > 0 { + v.Valid = false + } + + return containerValidation +} + // Close closes the Session container. func (s *Session) Close(ctx context.Context) error { if !is.Nil(s.container) { diff --git a/internal/bsr/bsr_validate_test.go b/internal/bsr/bsr_validate_test.go new file mode 100644 index 000000000..4da92e4f7 --- /dev/null +++ b/internal/bsr/bsr_validate_test.go @@ -0,0 +1,621 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package bsr + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/hashicorp/boundary/internal/bsr/internal/checksum" + "github.com/hashicorp/boundary/internal/bsr/internal/fstest" + "github.com/hashicorp/boundary/internal/bsr/kms" + "github.com/hashicorp/boundary/internal/storage" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBSR_Validate_ValidateBSR(t *testing.T) { + ctx := context.Background() + + connectionId := "test_connection" + channelId := "test_channel" + sessionId := "s_01234567881" + protocol := Protocol("TEST_VALIDATE_BSR_PROTOCOL") + fs := &fstest.MemFS{} + + err := RegisterSummaryAllocFunc(protocol, ChannelContainer, func(ctx context.Context) Summary { + return &BaseChannelSummary{Id: "chr_123456789", ConnectionRecordingId: connectionId} + }) + require.NoError(t, err) + + err = RegisterSummaryAllocFunc(protocol, SessionContainer, func(ctx context.Context) Summary { + return &BaseSessionSummary{Id: "s_123456789", ConnectionCount: 1} + }) + require.NoError(t, err) + + err = RegisterSummaryAllocFunc(protocol, ConnectionContainer, func(ctx context.Context) Summary { + return &BaseConnectionSummary{Id: "cr_123456789", ChannelCount: 1} + }) + require.NoError(t, err) + + // Setup keys + keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), sessionId) + require.NoError(t, err) + + keyFn := func(w kms.WrappedKeys) (kms.UnwrappedKeys, error) { + u := kms.UnwrappedKeys{ + BsrKey: keys.BsrKey, + PrivKey: keys.PrivKey, + } + return u, nil + } + + cases := []struct { + name string + protocol Protocol + storage storage.FS + sessionId string + sessionRecordingId string + expectedSessionChecksums ContainerChecksumValidation + expectedConnectionChecksums ContainerChecksumValidation + expectedChannelChecksums ContainerChecksumValidation + expectedSessionContainerSize int + expectedConnectionContainerSize int + expectedChannelContainerSize int + wantErr error + }{ + { + name: "valid BSR with multiple connections and channels", + sessionId: "s_01234567881", + sessionRecordingId: "sr_01234567881", + storage: func() storage.FS { + sessionRecordingId := "sr_01234567881" + // Set up session + srm := &SessionRecordingMeta{ + Id: sessionRecordingId, + Protocol: protocol, + } + sessionMeta := TestSessionMeta(sessionId) + + sesh, err := NewSession(ctx, srm, sessionMeta, fs, keys, WithSupportsMultiplex(true)) + require.NoError(t, err) + require.NotNil(t, sesh) + + // Encode session summary + sesh.EncodeSummary(ctx, &BaseSessionSummary{ + Id: channelId, + }) + + // Set up connection 1 + conn1, err := sesh.NewConnection(ctx, &ConnectionRecordingMeta{Id: connectionId + "_1"}) + require.NoError(t, err) + require.NotNil(t, conn1) + + // Encode connection 2 summary + err = conn1.EncodeSummary(ctx, &BaseConnectionSummary{ + Id: connectionId + ".1", + ChannelCount: 1, + }) + require.NoError(t, err) + + // Set up connection 2 + conn2, err := sesh.NewConnection(ctx, &ConnectionRecordingMeta{Id: connectionId + "_2"}) + require.NoError(t, err) + require.NotNil(t, conn1) + + // Encode connection 2 summary + err = conn2.EncodeSummary(ctx, &BaseConnectionSummary{ + Id: connectionId + ".2", + ChannelCount: 1, + }) + require.NoError(t, err) + + // Setup Channels with Connection 1 + ch1, err := conn1.NewChannel(ctx, &ChannelRecordingMeta{ + Id: channelId + "_1", + Type: "chan", + }) + require.NoError(t, err) + require.NotNil(t, ch1) + + err = ch1.EncodeSummary(ctx, &BaseChannelSummary{ + Id: channelId + "_1", + ConnectionRecordingId: connectionId + "_1", + }) + require.NoError(t, err) + + ch2, err := conn1.NewChannel(ctx, &ChannelRecordingMeta{ + Id: channelId + "_2", + Type: "chan", + }) + require.NoError(t, err) + require.NotNil(t, ch2) + + err = ch2.EncodeSummary(ctx, &BaseChannelSummary{ + Id: channelId + "_2", + ConnectionRecordingId: connectionId + "_1", + }) + require.NoError(t, err) + + // Setup Channels with Connection 2 + ch3, err := conn2.NewChannel(ctx, &ChannelRecordingMeta{ + Id: channelId + "_1", + Type: "chan", + }) + require.NoError(t, err) + require.NotNil(t, ch3) + + err = ch3.EncodeSummary(ctx, &BaseChannelSummary{ + Id: channelId + "_1", + ConnectionRecordingId: connectionId + "_2", + }) + require.NoError(t, err) + + ch4, err := conn2.NewChannel(ctx, &ChannelRecordingMeta{ + Id: channelId + "_2", + Type: "chan", + }) + require.NoError(t, err) + require.NotNil(t, ch4) + + // Encode channel summary + err = ch4.EncodeSummary(ctx, &BaseChannelSummary{ + Id: channelId + "_2", + ConnectionRecordingId: connectionId + "_2", + }) + require.NoError(t, err) + + require.NoError(t, ch1.Close(ctx)) + require.NoError(t, ch2.Close(ctx)) + require.NoError(t, ch3.Close(ctx)) + require.NoError(t, ch4.Close(ctx)) + require.NoError(t, conn1.Close(ctx)) + require.NoError(t, conn2.Close(ctx)) + require.NoError(t, sesh.Close(ctx)) + + return fs + }(), + expectedSessionChecksums: ContainerChecksumValidation{ + bsrPubKeyFileName: &FileChecksumValidation{ + Filename: bsrPubKeyFileName, + Passed: true, + }, + pubKeyBsrSignatureFileName: &FileChecksumValidation{ + Filename: pubKeyBsrSignatureFileName, + Passed: true, + }, + pubKeySelfSignatureFileName: &FileChecksumValidation{ + Filename: pubKeySelfSignatureFileName, + Passed: true, + }, + wrappedBsrKeyFileName: &FileChecksumValidation{ + Filename: wrappedBsrKeyFileName, + Passed: true, + }, + wrappedPrivKeyFileName: &FileChecksumValidation{ + Filename: wrappedPrivKeyFileName, + Passed: true, + }, + sessionMetaFileName: &FileChecksumValidation{ + Filename: sessionMetaFileName, + Passed: true, + }, + "session-recording.meta": &FileChecksumValidation{ + Filename: "session-recording.meta", + Passed: true, + }, + "session-recording-summary.json": &FileChecksumValidation{ + Filename: "session-recording-summary.json", + Passed: true, + }, + }, + expectedConnectionChecksums: ContainerChecksumValidation{ + "connection-recording-summary.json": &FileChecksumValidation{ + Filename: "connection-recording-summary.json", + Passed: true, + }, + "connection-recording.meta": &FileChecksumValidation{ + Filename: "connection-recording.meta", + Passed: true, + }, + }, + expectedChannelChecksums: ContainerChecksumValidation{ + "channel-recording-summary.json": &FileChecksumValidation{ + Filename: "channel-recording-summary.json", + Passed: true, + }, + "channel-recording.meta": &FileChecksumValidation{ + Filename: "channel-recording.meta", + Passed: true, + }, + }, + expectedSessionContainerSize: 2, + expectedConnectionContainerSize: 2, + expectedChannelContainerSize: 0, + }, + { + name: "Failed checksum", + sessionRecordingId: "sr_21234567881", + storage: func() storage.FS { + tmpFileName := "Test" + // Set up session + srm := &SessionRecordingMeta{ + Id: "sr_21234567881", + Protocol: protocol, + } + sessionMeta := TestSessionMeta(sessionId) + + session, err := NewSession(ctx, srm, sessionMeta, fs, keys, WithSupportsMultiplex(true)) + require.NoError(t, err) + require.NotNil(t, session) + + // Encode session summary + session.EncodeSummary(ctx, &BaseSessionSummary{ + Id: channelId, + }) + + testFile, err := session.container.container.Create(ctx, tmpFileName) + require.NoError(t, err) + + _, err = testFile.Write([]byte("hello world")) + require.NoError(t, err) + tfc, err := checksum.NewFile(ctx, testFile, session.checksums) + require.NoError(t, err) + require.NotNil(t, tfc) + require.NoError(t, tfc.Close()) + + require.NoError(t, session.Close(ctx)) + + openSesh, err := OpenSession(ctx, srm.Id, fs, keyFn) + require.NoError(t, err) + require.NotNil(t, openSesh) + + openedTestFile, err := openSesh.container.container.OpenFile(ctx, tmpFileName, storage.WithCreateFile(), storage.WithFileAccessMode(storage.ReadWrite)) + require.NoError(t, err) + + _, err = openedTestFile.Write([]byte("invalid text")) + require.NoError(t, err) + require.NoError(t, openedTestFile.Close()) + + return fs + }(), + + expectedSessionChecksums: ContainerChecksumValidation{ + bsrPubKeyFileName: &FileChecksumValidation{ + Filename: bsrPubKeyFileName, + Passed: true, + }, + pubKeyBsrSignatureFileName: &FileChecksumValidation{ + Filename: pubKeyBsrSignatureFileName, + Passed: true, + }, + pubKeySelfSignatureFileName: &FileChecksumValidation{ + Filename: pubKeySelfSignatureFileName, + Passed: true, + }, + wrappedBsrKeyFileName: &FileChecksumValidation{ + Filename: wrappedBsrKeyFileName, + Passed: true, + }, + wrappedPrivKeyFileName: &FileChecksumValidation{ + Filename: wrappedPrivKeyFileName, + Passed: true, + }, + sessionMetaFileName: &FileChecksumValidation{ + Filename: sessionMetaFileName, + Passed: true, + }, + "session-recording.meta": &FileChecksumValidation{ + Filename: "session-recording.meta", + Passed: true, + }, + "session-recording-summary.json": &FileChecksumValidation{ + Filename: "session-recording-summary.json", + Passed: true, + }, + "Test": &FileChecksumValidation{ + Filename: "Test", + Passed: false, + Error: errors.New("checksum mismatch"), + }, + }, + expectedConnectionChecksums: ContainerChecksumValidation{ + "connection-recording-summary.json": &FileChecksumValidation{ + Filename: "connection-recording-summary.json", + Passed: true, + }, + "connection-recording.meta": &FileChecksumValidation{ + Filename: "connection-recording.meta", + Passed: true, + }, + }, + expectedChannelChecksums: ContainerChecksumValidation{ + "channel-recording-summary.json": &FileChecksumValidation{ + Filename: "channel-recording-summary.json", + Passed: true, + }, + "channel-recording.meta": &FileChecksumValidation{ + Filename: "channel-recording.meta", + Passed: true, + }, + }, + expectedSessionContainerSize: 2, + expectedConnectionContainerSize: 2, + expectedChannelContainerSize: 0, + }, + { + name: "missing session recording id parameter", + wantErr: errors.New("bsr.Validate: missing session recording id: invalid parameter"), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + validation, err := Validate(ctx, tc.sessionRecordingId, tc.storage, keyFn) + if tc.wantErr != nil { + assert.EqualError(t, err, tc.wantErr.Error()) + return + } + require.NoError(t, err) + require.NotNil(t, validation) + + // Validate Session + assert.Equal(t, tc.sessionRecordingId, validation.SessionRecordingId) + assert.Equal(t, SessionContainer, validation.SessionRecordingValidation.ContainerType) + assert.Equal(t, len(tc.expectedSessionChecksums), len(validation.SessionRecordingValidation.FileChecksumValidations)) + assert.Equal(t, tc.expectedSessionChecksums, validation.SessionRecordingValidation.FileChecksumValidations) + + // Ensure session container is closed + sessionContainer := fs.Containers[fmt.Sprintf(bsrFileNameTemplate, validation.SessionRecordingId)] + require.NotNil(t, sessionContainer) + assert.True(t, sessionContainer.Closed) + + // Validate Multiple Connections + for _, connection := range validation.SessionRecordingValidation.SubContainers { + require.NotNil(t, connection) + assert.Equal(t, ConnectionContainer, connection.ContainerType) + assert.Equal(t, tc.expectedConnectionContainerSize, len(connection.SubContainers)) + assert.Equal(t, len(tc.expectedConnectionChecksums), len(connection.FileChecksumValidations)) + assert.Equal(t, tc.expectedConnectionChecksums, connection.FileChecksumValidations) + + // Ensure connection container is closed + connectionContainer := sessionContainer.Sub[fmt.Sprintf(connectionFileNameTemplate, connection.Name)] + require.NotNil(t, connectionContainer) + assert.True(t, connectionContainer.Closed) + + // Validate Multiple Channels + for _, channel := range connection.SubContainers { + require.NotNil(t, channel) + assert.Equal(t, ChannelContainer, channel.ContainerType) + assert.Equal(t, tc.expectedChannelContainerSize, len(channel.SubContainers)) + assert.Equal(t, len(tc.expectedChannelChecksums), len(channel.FileChecksumValidations)) + assert.Equal(t, tc.expectedChannelChecksums, channel.FileChecksumValidations) + + // Ensure channel container is closed + channelContainer := connectionContainer.Sub[fmt.Sprintf(channelFileNameTemplate, channel.Name)] + require.NotNil(t, channelContainer) + assert.True(t, channelContainer.Closed) + } + } + }) + } +} + +func TestBSR_Validate_ValidateContainer(t *testing.T) { + ctx := context.Background() + + protocol := Protocol("TEST_VALIDATE_CONTAINER_PROTOCOL") + connectionId := "test_connection" + channelId := "chr_123456789" + sessionId := "s_01234567881" + + err := RegisterSummaryAllocFunc(protocol, ChannelContainer, func(ctx context.Context) Summary { + return &BaseChannelSummary{Id: channelId, ConnectionRecordingId: connectionId} + }) + require.NoError(t, err) + + err = RegisterSummaryAllocFunc(protocol, SessionContainer, func(ctx context.Context) Summary { + return &BaseSessionSummary{Id: sessionId, ConnectionCount: 1} + }) + require.NoError(t, err) + + err = RegisterSummaryAllocFunc(protocol, ConnectionContainer, func(ctx context.Context) Summary { + return &BaseConnectionSummary{Id: connectionId, ChannelCount: 1} + }) + require.NoError(t, err) + + cases := []struct { + name string + c *container + containerName string + containerType ContainerType + expectedValidation *Validation + expectedContainerValidation *ContainerValidation + wantErr error + }{ + { + name: "valid container", + c: func() *container { + fs := &fstest.MemFS{} + + // Setup keys + keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), sessionId) + require.NoError(t, err) + + keyFn := func(w kms.WrappedKeys) (kms.UnwrappedKeys, error) { + u := kms.UnwrappedKeys{ + BsrKey: keys.BsrKey, + PrivKey: keys.PrivKey, + } + return u, nil + } + + // Set up session + srm := &SessionRecordingMeta{ + Id: sessionId, + Protocol: protocol, + } + sessionMeta := TestSessionMeta(sessionId) + + session, err := NewSession(ctx, srm, sessionMeta, fs, keys, WithSupportsMultiplex(true)) + require.NoError(t, err) + require.NotNil(t, session) + + // Encode session summary + session.EncodeSummary(ctx, &BaseSessionSummary{ + Id: channelId, + }) + + require.NoError(t, session.Close(ctx)) + + openSesh, err := OpenSession(ctx, srm.Id, fs, keyFn) + require.NoError(t, err) + require.NotNil(t, openSesh) + + return openSesh.container + }(), + containerName: "sr_123456789", + containerType: SessionContainer, + expectedValidation: &Validation{ + SessionRecordingId: "sr_123456789", + Valid: true, + }, + expectedContainerValidation: &ContainerValidation{ + Name: "sr_123456789", + ContainerType: SessionContainer, + FileChecksumValidations: ContainerChecksumValidation{ + bsrPubKeyFileName: &FileChecksumValidation{ + Filename: bsrPubKeyFileName, + Passed: true, + }, + pubKeyBsrSignatureFileName: &FileChecksumValidation{ + Filename: pubKeyBsrSignatureFileName, + Passed: true, + }, + pubKeySelfSignatureFileName: &FileChecksumValidation{ + Filename: pubKeySelfSignatureFileName, + Passed: true, + }, + wrappedBsrKeyFileName: &FileChecksumValidation{ + Filename: wrappedBsrKeyFileName, + Passed: true, + }, + wrappedPrivKeyFileName: &FileChecksumValidation{ + Filename: wrappedPrivKeyFileName, + Passed: true, + }, + sessionMetaFileName: &FileChecksumValidation{ + Filename: sessionMetaFileName, + Passed: true, + }, + "session-recording.meta": &FileChecksumValidation{ + Filename: "session-recording.meta", + Passed: true, + }, + "session-recording-summary.json": &FileChecksumValidation{ + Filename: "session-recording-summary.json", + Passed: true, + }, + }, + }, + }, + { + name: "invalid container", + c: func() *container { + sessionId := "session_123456789" + keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), sessionId) + require.NoError(t, err) + + fs := &fstest.MemFS{} + sc, err := fs.New(ctx, fmt.Sprintf(bsrFileNameTemplate, sessionId)) + require.NoError(t, err) + + c, err := newContainer(ctx, SessionContainer, sc, keys) + require.NoError(t, err) + + c.shaSums = checksum.Sha256Sums{ + "test": []byte("test"), + } + + f, err := c.create(ctx, "test") + require.NoError(t, err) + _, err = f.Write([]byte("hello world")) + require.NoError(t, err) + require.NoError(t, f.Close()) + + return c + }(), + containerName: "sr_123456789", + containerType: SessionContainer, + expectedValidation: &Validation{ + SessionRecordingId: "sr_123456789", + Valid: false, + }, + expectedContainerValidation: &ContainerValidation{ + Name: "sr_123456789", + ContainerType: SessionContainer, + FileChecksumValidations: ContainerChecksumValidation{ + "test": &FileChecksumValidation{ + Filename: "test", + Passed: false, + Error: errors.New("checksum mismatch"), + }, + }, + }, + }, + { + name: "invalid container - without checksums", + c: func() *container { + sessionId := "session_123456789" + keys, err := kms.CreateKeys(ctx, kms.TestWrapper(t), sessionId) + require.NoError(t, err) + + fs := &fstest.MemFS{} + sc, err := fs.New(ctx, fmt.Sprintf(bsrFileNameTemplate, sessionId)) + require.NoError(t, err) + + c, err := newContainer(ctx, SessionContainer, sc, keys) + require.NoError(t, err) + + f, err := c.create(ctx, "test") + require.NoError(t, err) + _, err = f.Write([]byte("hello world")) + require.NoError(t, err) + require.NoError(t, f.Close()) + + return c + }(), + containerName: "sr_123456789", + containerType: SessionContainer, + expectedValidation: &Validation{ + SessionRecordingId: "sr_123456789", + Valid: false, + }, + expectedContainerValidation: &ContainerValidation{ + Name: "sr_123456789", + ContainerType: SessionContainer, + }, + wantErr: errors.New("bsr.(Validate).ValidateContainer: failed to validate sr_123456789: bsr.(container).Validate: missing checksums"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + v := &Validation{ + SessionRecordingId: tc.containerName, + Valid: true, + } + containerValidation := validateContainer(ctx, v, tc.containerType, tc.c, tc.containerName) + if tc.wantErr != nil { + assert.EqualError(t, containerValidation.Error, tc.wantErr.Error()) + return + } + require.NotNil(t, containerValidation) + + assert.Equal(t, v, tc.expectedValidation) + assert.Equal(t, tc.expectedContainerValidation, containerValidation) + }) + } +}