Files
teleport/lib/srv/db/sqlserver/engine_test.go
T
Krzysztof SkrzętnickiandSTeve Huang 18fe621bab convert db lib to use slog, part 2 (#43849)
* Switch Engine, EngineConfig and SessionContext to slog.

* Apply suggestions from code review

Co-authored-by: STeve (Xin) Huang <xin.huang@goteleport.com>

* fix some lint issues, restore trace logs,

* fix tests

* fix some more lint issues

---------

Co-authored-by: STeve (Xin) Huang <xin.huang@goteleport.com>
2024-07-04 15:22:30 +00:00

448 lines
13 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package sqlserver
import (
"bytes"
"context"
"crypto/tls"
"io"
"log/slog"
"net"
"testing"
"time"
"github.com/google/go-cmp/cmp"
mssql "github.com/microsoft/go-mssqldb"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/events"
libevents "github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/events/eventstest"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/readonly"
"github.com/gravitational/teleport/lib/srv/db/common"
"github.com/gravitational/teleport/lib/srv/db/sqlserver/protocol"
"github.com/gravitational/teleport/lib/srv/db/sqlserver/protocol/fixtures"
)
// TestHandleConnectionAuditEvents checks audit events emitted during HandleConnection execution.
func TestHandleConnectionAuditEvents(t *testing.T) {
type check func(t *testing.T, err error, ee []events.AuditEvent)
hasNoErr := func() check {
return func(t *testing.T, err error, ee []events.AuditEvent) {
require.NoError(t, err)
}
}
hasAuditEventCode := func(want string) check {
return func(t *testing.T, err error, ee []events.AuditEvent) {
for _, v := range ee {
if v.GetCode() == want {
return
}
}
require.Failf(t, "event not found", "event code: %s", want)
}
}
hasAuditEvent := func(i int, want events.AuditEvent) check {
return func(t *testing.T, err error, ee []events.AuditEvent) {
diff := cmp.Diff(want, ee[i])
require.Empty(t, diff)
}
}
tests := []struct {
name string
packets [][]byte
checks []check
}{
{
name: "rpc request procedure",
packets: [][]byte{fixtures.GenerateCustomRPCCallPacket("foo3")},
checks: []check{
hasNoErr(),
hasAuditEventCode(libevents.DatabaseSessionStartCode),
hasAuditEventCode(libevents.DatabaseSessionEndCode),
hasAuditEvent(1, &events.SQLServerRPCRequest{
DatabaseMetadata: events.DatabaseMetadata{
DatabaseUser: "sa",
DatabaseType: "self-hosted",
DatabaseService: "dummy",
DatabaseURI: "uri",
DatabaseProtocol: "test",
},
Metadata: events.Metadata{
Type: libevents.DatabaseSessionSQLServerRPCRequestEvent,
Code: libevents.SQLServerRPCRequestCode,
},
UserMetadata: events.UserMetadata{
UserKind: events.UserKind_USER_KIND_HUMAN,
},
Procname: "foo3",
}),
},
},
{
name: "rpc request param",
packets: [][]byte{fixtures.GenerateExecuteSQLRPCPacket("select @@version")},
checks: []check{
hasNoErr(),
hasAuditEventCode(libevents.DatabaseSessionStartCode),
hasAuditEventCode(libevents.DatabaseSessionEndCode),
hasAuditEvent(1, &events.SQLServerRPCRequest{
DatabaseMetadata: events.DatabaseMetadata{
DatabaseUser: "sa",
DatabaseType: "self-hosted",
DatabaseService: "dummy",
DatabaseURI: "uri",
DatabaseProtocol: "test",
},
Metadata: events.Metadata{
Type: libevents.DatabaseSessionSQLServerRPCRequestEvent,
Code: libevents.SQLServerRPCRequestCode,
},
UserMetadata: events.UserMetadata{
UserKind: events.UserKind_USER_KIND_HUMAN,
},
Parameters: []string{"select @@version"},
Procname: "Sp_ExecuteSql",
}),
},
},
{
name: "sql batch",
packets: [][]byte{fixtures.GenerateBatchQueryPacket("\nselect 'foo' as 'bar'\n ")},
checks: []check{
hasNoErr(),
hasAuditEventCode(libevents.DatabaseSessionStartCode),
hasAuditEventCode(libevents.DatabaseSessionEndCode),
hasAuditEvent(1, &events.DatabaseSessionQuery{
DatabaseMetadata: events.DatabaseMetadata{
DatabaseUser: "sa",
DatabaseType: "self-hosted",
DatabaseService: "dummy",
DatabaseURI: "uri",
DatabaseProtocol: "test",
},
Metadata: events.Metadata{
Type: libevents.DatabaseSessionQueryEvent,
Code: libevents.DatabaseSessionQueryCode,
},
UserMetadata: events.UserMetadata{
UserKind: events.UserKind_USER_KIND_HUMAN,
},
DatabaseQuery: "\nselect 'foo' as 'bar'\n ",
Status: events.Status{
Success: true,
},
}),
},
},
{
name: "malformed packet",
packets: [][]byte{fixtures.MalformedPacketTest},
checks: []check{
hasNoErr(),
hasAuditEventCode(libevents.DatabaseSessionStartCode),
hasAuditEventCode(libevents.DatabaseSessionEndCode),
hasAuditEventCode(libevents.DatabaseSessionMalformedPacketCode),
},
},
{
name: "sql batch chunked packets",
packets: fixtures.GenerateBatchQueryChunkedPacket(5, "select 'foo' as 'bar'"),
checks: []check{
hasNoErr(),
hasAuditEventCode(libevents.DatabaseSessionStartCode),
hasAuditEventCode(libevents.DatabaseSessionEndCode),
hasAuditEvent(1, &events.DatabaseSessionQuery{
DatabaseMetadata: events.DatabaseMetadata{
DatabaseUser: "sa",
DatabaseType: "self-hosted",
DatabaseService: "dummy",
DatabaseURI: "uri",
DatabaseProtocol: "test",
},
Metadata: events.Metadata{
Type: libevents.DatabaseSessionQueryEvent,
Code: libevents.DatabaseSessionQueryCode,
},
UserMetadata: events.UserMetadata{
UserKind: events.UserKind_USER_KIND_HUMAN,
},
DatabaseQuery: "select 'foo' as 'bar'",
Status: events.Status{
Success: true,
},
}),
},
},
{
name: "rpc request param chunked",
packets: fixtures.GenerateExecuteSQLRPCChunkedPacket(5, "select @@version"),
checks: []check{
hasNoErr(),
hasAuditEventCode(libevents.DatabaseSessionStartCode),
hasAuditEventCode(libevents.DatabaseSessionEndCode),
hasAuditEvent(1, &events.SQLServerRPCRequest{
DatabaseMetadata: events.DatabaseMetadata{
DatabaseUser: "sa",
DatabaseType: "self-hosted",
DatabaseService: "dummy",
DatabaseURI: "uri",
DatabaseProtocol: "test",
},
Metadata: events.Metadata{
Type: libevents.DatabaseSessionSQLServerRPCRequestEvent,
Code: libevents.SQLServerRPCRequestCode,
},
UserMetadata: events.UserMetadata{
UserKind: events.UserKind_USER_KIND_HUMAN,
},
Parameters: []string{"select @@version"},
Procname: "Sp_ExecuteSql",
}),
},
},
{
name: "intercalated chunked messages",
packets: intercalateChunkedPacketMessages(
fixtures.GenerateExecuteSQLRPCChunkedPacket(5, "select @@version"),
fixtures.GenerateExecuteSQLRPCPacket("select 1"),
2,
),
checks: []check{
hasNoErr(),
hasAuditEventCode(libevents.DatabaseSessionStartCode),
hasAuditEventCode(libevents.DatabaseSessionEndCode),
hasAuditEvent(1, &events.SQLServerRPCRequest{
DatabaseMetadata: events.DatabaseMetadata{
DatabaseUser: "sa",
DatabaseType: "self-hosted",
DatabaseService: "dummy",
DatabaseURI: "uri",
DatabaseProtocol: "test",
},
Metadata: events.Metadata{
Type: libevents.DatabaseSessionSQLServerRPCRequestEvent,
Code: libevents.SQLServerRPCRequestCode,
},
UserMetadata: events.UserMetadata{
UserKind: events.UserKind_USER_KIND_HUMAN,
},
Parameters: []string{"select @@version"},
Procname: "Sp_ExecuteSql",
}),
hasAuditEvent(2, &events.SQLServerRPCRequest{
DatabaseMetadata: events.DatabaseMetadata{
DatabaseUser: "sa",
DatabaseType: "self-hosted",
DatabaseService: "dummy",
DatabaseURI: "uri",
DatabaseProtocol: "test",
},
Metadata: events.Metadata{
Type: libevents.DatabaseSessionSQLServerRPCRequestEvent,
Code: libevents.SQLServerRPCRequestCode,
},
UserMetadata: events.UserMetadata{
UserKind: events.UserKind_USER_KIND_HUMAN,
},
Parameters: []string{"select 1"},
Procname: "Sp_ExecuteSql",
}),
hasAuditEvent(3, &events.SQLServerRPCRequest{
DatabaseMetadata: events.DatabaseMetadata{
DatabaseUser: "sa",
DatabaseType: "self-hosted",
DatabaseService: "dummy",
DatabaseURI: "uri",
DatabaseProtocol: "test",
},
Metadata: events.Metadata{
Type: libevents.DatabaseSessionSQLServerRPCRequestEvent,
Code: libevents.SQLServerRPCRequestCode,
},
UserMetadata: events.UserMetadata{
UserKind: events.UserKind_USER_KIND_HUMAN,
},
Parameters: []string{"select @@version"},
Procname: "Sp_ExecuteSql",
}),
hasAuditEvent(4, &events.SQLServerRPCRequest{
DatabaseMetadata: events.DatabaseMetadata{
DatabaseUser: "sa",
DatabaseType: "self-hosted",
DatabaseService: "dummy",
DatabaseURI: "uri",
DatabaseProtocol: "test",
},
Metadata: events.Metadata{
Type: libevents.DatabaseSessionSQLServerRPCRequestEvent,
Code: libevents.SQLServerRPCRequestCode,
},
UserMetadata: events.UserMetadata{
UserKind: events.UserKind_USER_KIND_HUMAN,
},
Parameters: []string{"select 1"},
Procname: "Sp_ExecuteSql",
}),
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var b bytes.Buffer
_, err := b.Write(fixtures.Login7)
require.NoError(t, err)
db, err := types.NewDatabaseV3(types.Metadata{
Name: "dummy",
Labels: map[string]string{"env": "prod"},
}, types.DatabaseSpecV3{
Protocol: "test",
URI: "uri",
})
require.NoError(t, err)
for _, packet := range tc.packets {
_, err = b.Write(packet)
require.NoError(t, err)
}
emitterMock := &eventstest.MockRecorderEmitter{}
audit, err := common.NewAudit(common.AuditConfig{
Emitter: emitterMock,
Recorder: libevents.WithNoOpPreparer(libevents.NewDiscardRecorder()),
Database: db,
})
require.NoError(t, err)
e := Engine{
EngineConfig: common.EngineConfig{
Audit: audit,
Log: slog.Default(),
Auth: &mockDBAuth{},
Context: context.Background(),
},
Connector: &mockConnector{
conn: &mockConn{
buff: bytes.Buffer{},
},
},
clientConn: &mockConn{
buff: b,
},
}
err = e.HandleConnection(context.Background(), &common.Session{
Checker: &mockChecker{},
Database: db,
})
for _, ch := range tc.checks {
ch(t, err, emitterMock.Events())
}
})
}
}
// intercalateChunkedPacketMessages intercalates a chunked packet with a regular packet a specified number of times.
func intercalateChunkedPacketMessages(chunkedPacket [][]byte, regularPacket []byte, repeat int) [][]byte {
var result [][]byte
for i := 0; i < repeat; i++ {
result = append(result, chunkedPacket...)
result = append(result, regularPacket)
}
return result
}
type mockConn struct {
net.Conn
buff bytes.Buffer
}
func (o *mockConn) Read(p []byte) (n int, err error) {
return o.buff.Read(p)
}
func (o *mockConn) Write(p []byte) (n int, err error) {
return len(p), nil
}
func (o *mockConn) Close() error {
return nil
}
type mockDBAuth struct {
common.Auth
// GetAzureIdentityResourceID mocks.
azureIdentityResourceID string
azureIdentityResourceIDErr error
}
func (m *mockDBAuth) GetAuthPreference(ctx context.Context) (types.AuthPreference, error) {
return types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorWebauthn,
Webauthn: &types.Webauthn{
RPID: "localhost",
},
RequireMFAType: types.RequireMFAType_SESSION,
})
}
func (m *mockDBAuth) GetTLSConfig(ctx context.Context, certExpiry time.Time, database types.Database, databaseUser string) (*tls.Config, error) {
return &tls.Config{}, nil
}
func (m *mockDBAuth) GetAzureIdentityResourceID(_ context.Context, _ string) (string, error) {
return m.azureIdentityResourceID, m.azureIdentityResourceIDErr
}
type mockChecker struct {
services.AccessChecker
}
func (m *mockChecker) CheckAccess(r services.AccessCheckable, state services.AccessState, matchers ...services.RoleMatcher) error {
return nil
}
func (m *mockChecker) GetAccessState(authPref readonly.AuthPreference) services.AccessState {
if authPref.GetRequireMFAType().IsSessionMFARequired() {
return services.AccessState{
MFARequired: services.MFARequiredAlways,
}
}
return services.AccessState{
MFARequired: services.MFARequiredNever,
}
}
type mockConnector struct {
conn io.ReadWriteCloser
}
func (m *mockConnector) Connect(context.Context, *common.Session, *protocol.Login7Packet) (io.ReadWriteCloser, []mssql.Token, error) {
return m.conn, []mssql.Token{}, nil
}