Files
threshold/internal/session/testing.go
T
Timothy Messier 37bf59243a refact(session): Replace state start_time/end_time with time range (#5134)
This replaces the start_time, end_time, and previous_end_time columns on
the session_state table with a time range column that records when the
state was active. This allowed for dropping several constraints on the
session_state table that were used to ensure that states for a session
did not overlap. Instead this can be ensured with a single exclusion
constraint. By reducing the number of constraints, it improves the
performance for a number of queries involved with session state. Most
notably when resolving delete cascades of resources related to sessions,
such as targets, where as part of the delete transaction large numbers
of sessions get canceled. Similarly, when sessions get deleted, and the
corresponding session_state rows are deleted, these constraints all must
be checked during the transaction.

The domain layer continues to expose StartTime and EndTime fields on
session.State. However it drops PreviousEndTime, which was never used by
anything calling the domain layer.

See:
    https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-EXCLUSION
    https://www.postgresql.org/docs/current/rangetypes.html
2024-09-30 14:39:19 -04:00

288 lines
10 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package session
import (
"context"
"crypto/ed25519"
"crypto/rand"
"testing"
"time"
"github.com/hashicorp/boundary/internal/auth/password"
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/host/static"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/target"
"github.com/hashicorp/boundary/internal/target/tcp"
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
"github.com/hashicorp/go-secure-stdlib/base62"
"github.com/hashicorp/go-uuid"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
)
// TestConnection creates a test connection for the sessionId in the repository.
func TestConnection(t testing.TB, conn *db.DB, sessionId, clientTcpAddr string, clientTcpPort uint32, endpointTcpAddr string, endpointTcpPort uint32, userClientIp string) *Connection {
t.Helper()
ctx := context.Background()
require := require.New(t)
rw := db.New(conn)
c, err := NewConnection(ctx, sessionId, clientTcpAddr, clientTcpPort, endpointTcpAddr, endpointTcpPort, userClientIp)
require.NoError(err)
id, err := newConnectionId(ctx)
require.NoError(err)
c.PublicId = id
err = rw.Create(ctx, c)
require.NoError(err)
return c
}
// TestState creates a test state for the sessionId in the repository.
func TestState(t testing.TB, conn *db.DB, sessionId string, state Status) *State {
const insertSessionState = `
insert into session_state (session_id, state, active_time_range)
values ($1, $2, tstzrange($3, null, '[]'))
returning lower(active_time_range) as start_time
;`
t.Helper()
require := require.New(t)
rw := db.New(conn)
s, err := NewState(context.Background(), sessionId, state)
require.NoError(err)
rows, err := rw.Query(context.Background(), insertSessionState, []any{s.SessionId, s.Status, s.StartTime})
require.NoError(err)
defer rows.Close()
for rows.Next() {
err := rows.Scan(&s.StartTime)
require.NoError(err)
}
return s
}
// TestSessionHostSetHost creates a test session to host set host association for the sessionId in the repository.
func TestSessionHostSetHost(t testing.TB, conn *db.DB, sessionId, hostSetId, hostId string) {
t.Helper()
require := require.New(t)
rw := db.New(conn)
hs, err := NewSessionHostSetHost(context.Background(), sessionId, hostSetId, hostId)
require.NoError(err)
err = rw.Create(context.Background(), hs)
require.NoError(err)
}
// TestSessionTargetAddress creates a test session to target address association for the sessionId in the repository.
func TestSessionTargetAddress(t testing.TB, conn *db.DB, sessionId, targetId string) {
t.Helper()
require := require.New(t)
rw := db.New(conn)
ta, err := NewSessionTargetAddress(context.Background(), sessionId, targetId)
require.NoError(err)
err = rw.Create(context.Background(), ta)
require.NoError(err)
}
// TestSession creates a test session composed of c in the repository. Options
// are passed into New, and withServerId is handled locally.
func TestSession(t testing.TB, conn *db.DB, rootWrapper wrapping.Wrapper, c ComposedOf, opt ...Option) *Session {
t.Helper()
ctx := context.Background()
opts := getOpts(opt...)
require := require.New(t)
if c.ExpirationTime == nil {
future := timestamppb.New(time.Now().Add(time.Hour))
c.ExpirationTime = &timestamp.Timestamp{Timestamp: future}
}
if c.CorrelationId == "" {
correlationId, err := uuid.GenerateUUID()
require.NoError(err)
c.CorrelationId = correlationId
}
rw := db.New(conn)
s, err := New(ctx, c, opt...)
require.NoError(err)
id, err := newId(ctx)
require.NoError(err)
s.PublicId = id
kmsCache := kms.TestKms(t, conn, rootWrapper)
wrapper, err := kmsCache.GetWrapper(ctx, c.ProjectId, kms.KeyPurposeSessions)
require.NoError(err)
privateKey, certBytes, err := newCert(ctx, id, []string{"127.0.0.1", "localhost"}, c.ExpirationTime.Timestamp.AsTime(), rand.Reader)
require.NoError(err)
s.Certificate = certBytes
s.CertificatePrivateKey = privateKey
err = s.encrypt(ctx, wrapper)
require.NoError(err)
err = rw.Create(ctx, s, opts.withDbOpts...)
if e, ok := err.(*errors.Err); err != nil && ok && e.Code == errors.NotUnique {
// Sometimes we can get unique constraint errors when creating a session,
// if that happens, just retry.
err = rw.Create(ctx, s, opts.withDbOpts...)
}
require.NoError(err)
for _, cred := range s.DynamicCredentials {
cred.SessionId = s.PublicId
err := rw.Create(ctx, cred)
require.NoError(err)
}
for _, cred := range s.StaticCredentials {
cred.SessionId = s.PublicId
err := rw.Create(ctx, cred)
require.NoError(err)
}
if s.HostId != "" && s.HostSetId != "" {
TestSessionHostSetHost(t, conn, s.PublicId, s.HostSetId, s.HostId)
} else if s.Endpoint != "" {
TestSessionTargetAddress(t, conn, s.PublicId, s.TargetId)
}
if s.ProtocolWorkerId != "" {
p, err := NewSessionWorkerProtocol(ctx, s.PublicId, s.ProtocolWorkerId)
require.NoError(err)
err = rw.Create(ctx, p)
require.NoError(err)
}
ss, err := fetchStates(ctx, rw, s.PublicId, opts.withDbOpts...)
require.NoError(err)
s.States = ss
return s
}
func TestSessionWithTargetAddress(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, iamRepo *iam.Repository, opt ...Option) *Session {
t.Helper()
composedOf := TestSessionTargetAddressParams(t, conn, wrapper, iamRepo)
future := timestamppb.New(time.Now().Add(time.Hour))
exp := &timestamp.Timestamp{Timestamp: future}
return TestSession(t, conn, wrapper, composedOf, append(opt, WithExpirationTime(exp))...)
}
func TestSessionTargetAddressParams(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, iamRepo *iam.Repository) ComposedOf {
t.Helper()
ctx := context.Background()
require := require.New(t)
rw := db.New(conn)
org, proj := iam.TestScopes(t, iamRepo)
tcpTarget := tcp.TestTarget(ctx, t, conn, proj.PublicId, "test target")
target.TestTargetAddress(t, conn, tcpTarget.GetPublicId(), "tcp://127.0.0.1:22")
kms := kms.TestKms(t, conn, wrapper)
authMethod := password.TestAuthMethods(t, conn, org.PublicId, 1)[0]
acct := password.TestAccount(t, conn, authMethod.GetPublicId(), "name1")
user := iam.TestUser(t, iamRepo, org.PublicId, iam.WithAccountIds(acct.PublicId))
authTokenRepo, err := authtoken.NewRepository(ctx, rw, rw, kms)
require.NoError(err)
at, err := authTokenRepo.CreateAuthToken(ctx, user, acct.GetPublicId())
require.NoError(err)
expTime := timestamppb.Now()
expTime.Seconds += int64(tcpTarget.GetSessionMaxSeconds())
correlationId, err := uuid.GenerateUUID()
require.NoError(err)
return ComposedOf{
UserId: user.PublicId,
TargetId: tcpTarget.GetPublicId(),
AuthTokenId: at.PublicId,
ProjectId: tcpTarget.GetProjectId(),
Endpoint: "tcp://127.0.0.1:22",
ExpirationTime: &timestamp.Timestamp{Timestamp: expTime},
ConnectionLimit: tcpTarget.GetSessionConnectionLimit(),
CorrelationId: correlationId,
}
}
// TestDefaultSession creates a test session in the repository using defaults.
func TestDefaultSession(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, iamRepo *iam.Repository, opt ...Option) *Session {
t.Helper()
composedOf := TestSessionParams(t, conn, wrapper, iamRepo)
future := timestamppb.New(time.Now().Add(time.Hour))
exp := &timestamp.Timestamp{Timestamp: future}
return TestSession(t, conn, wrapper, composedOf, append(opt, WithExpirationTime(exp))...)
}
// TestSessionParams returns an initialized ComposedOf which can be used to
// create a session in the repository.
func TestSessionParams(t testing.TB, conn *db.DB, wrapper wrapping.Wrapper, iamRepo *iam.Repository) ComposedOf {
t.Helper()
ctx := context.Background()
require := require.New(t)
rw := db.New(conn)
org, proj := iam.TestScopes(t, iamRepo)
cats := static.TestCatalogs(t, conn, proj.PublicId, 1)
hosts := static.TestHosts(t, conn, cats[0].PublicId, 1)
sets := static.TestSets(t, conn, cats[0].PublicId, 1)
_ = static.TestSetMembers(t, conn, sets[0].PublicId, hosts)
tcpTarget := tcp.TestTarget(ctx, t, conn, proj.PublicId, "test target")
kms := kms.TestKms(t, conn, wrapper)
targetRepo, err := target.NewRepository(ctx, rw, rw, kms)
require.NoError(err)
_, err = targetRepo.AddTargetHostSources(ctx, tcpTarget.GetPublicId(), tcpTarget.GetVersion(), []string{sets[0].PublicId})
require.NoError(err)
authMethod := password.TestAuthMethods(t, conn, org.PublicId, 1)[0]
acct := password.TestAccount(t, conn, authMethod.GetPublicId(), "name1")
user := iam.TestUser(t, iamRepo, org.PublicId, iam.WithAccountIds(acct.PublicId))
authTokenRepo, err := authtoken.NewRepository(ctx, rw, rw, kms)
require.NoError(err)
at, err := authTokenRepo.CreateAuthToken(ctx, user, acct.GetPublicId())
require.NoError(err)
expTime := timestamppb.Now()
expTime.Seconds += int64(tcpTarget.GetSessionMaxSeconds())
correlationId, err := uuid.GenerateUUID()
require.NoError(err)
return ComposedOf{
UserId: user.PublicId,
HostId: hosts[0].PublicId,
TargetId: tcpTarget.GetPublicId(),
HostSetId: sets[0].PublicId,
AuthTokenId: at.PublicId,
ProjectId: tcpTarget.GetProjectId(),
Endpoint: "tcp://127.0.0.1:22",
ExpirationTime: &timestamp.Timestamp{Timestamp: expTime},
ConnectionLimit: tcpTarget.GetSessionConnectionLimit(),
CorrelationId: correlationId,
}
}
// TestTofu will create a test "trust on first use" token
func TestTofu(t testing.TB) []byte {
t.Helper()
require := require.New(t)
tofu, err := base62.Random(20)
require.NoError(err)
return []byte(tofu)
}
// TestCert is a temporary test func that intentionally doesn't take testing.T
// as a parameter. It's currently used in controller.jobTestingHandler() and
// should be deprecated once that function is refactored to use sessions properly.
func TestCert(jobId string) (ed25519.PrivateKey, []byte, error) {
return newCert(context.Background(), jobId, []string{"127.0.0.1", "localhost"}, time.Now().Add(5*time.Minute), rand.Reader)
}
// TestListSessions returns a list of sessions and the timestamp of the query for testing purposes
func TestListSessions(t testing.TB, ctx context.Context, repo *Repository) ([]*Session, time.Time) {
sess, ttime, err := repo.listSessions(ctx)
require.NoError(t, err)
return sess, ttime
}