diff --git a/testing/internal/e2e/boundary/boundary.go b/testing/internal/e2e/boundary/boundary.go index 0878711d44..48029abb7e 100644 --- a/testing/internal/e2e/boundary/boundary.go +++ b/testing/internal/e2e/boundary/boundary.go @@ -39,6 +39,11 @@ type DbInitInfo struct { AuthMethod AuthMethodInfo `json:"auth_method"` } +// CliError parses the Stderr from running a boundary command +type CliError struct { + Status int `json:"status"` +} + func loadConfig() (*config, error) { var c config err := envconfig.Process("", &c) diff --git a/testing/internal/e2e/helpers.go b/testing/internal/e2e/helpers.go index 9595e678a4..34dc15a2c7 100644 --- a/testing/internal/e2e/helpers.go +++ b/testing/internal/e2e/helpers.go @@ -18,11 +18,6 @@ type CommandResult struct { Err error } -// CliError parses the Stderr from running a boundary command -type CliError struct { - Status int `json:"status"` -} - // Option is a func that sets optional attributes for a call. This does not need // to be used directly, but instead option arguments are built from the // functions in this package. WithX options set a value to that given in the diff --git a/testing/internal/e2e/tests/static/credential_store_test.go b/testing/internal/e2e/tests/static/credential_store_test.go index 44707813cb..0bb5d015d9 100644 --- a/testing/internal/e2e/tests/static/credential_store_test.go +++ b/testing/internal/e2e/tests/static/credential_store_test.go @@ -85,7 +85,7 @@ func TestCliStaticCredentialStore(t *testing.T) { return fmt.Errorf("Deleted credential can still be read: '%s'", output.Stdout) } - var response e2e.CliError + var response boundary.CliError err := json.Unmarshal(output.Stderr, &response) require.NoError(t, err) statusCode := response.Status diff --git a/testing/internal/e2e/tests/static/session_cancel_group_test.go b/testing/internal/e2e/tests/static/session_cancel_group_test.go index 7d6ea01a50..40fd350bdb 100644 --- a/testing/internal/e2e/tests/static/session_cancel_group_test.go +++ b/testing/internal/e2e/tests/static/session_cancel_group_test.go @@ -80,7 +80,7 @@ func TestCliSessionCancelGroup(t *testing.T) { ), ) require.Error(t, output.Err, string(output.Stdout), string(output.Stderr)) - var response e2e.CliError + var response boundary.CliError err = json.Unmarshal(output.Stderr, &response) require.NoError(t, err) require.Equal(t, 403, int(response.Status)) diff --git a/testing/internal/e2e/tests/static/session_cancel_user_test.go b/testing/internal/e2e/tests/static/session_cancel_user_test.go index 4ef3260837..48f3b5e164 100644 --- a/testing/internal/e2e/tests/static/session_cancel_user_test.go +++ b/testing/internal/e2e/tests/static/session_cancel_user_test.go @@ -79,7 +79,7 @@ func TestCliSessionCancelUser(t *testing.T) { ), ) require.Error(t, output.Err, string(output.Stdout), string(output.Stderr)) - var response e2e.CliError + var response boundary.CliError err = json.Unmarshal(output.Stderr, &response) require.NoError(t, err) require.Equal(t, 403, int(response.Status)) diff --git a/testing/internal/e2e/tests/static/session_end_delete_host_set_test.go b/testing/internal/e2e/tests/static/session_end_delete_host_set_test.go index 5a04a66f93..e09623f9f0 100644 --- a/testing/internal/e2e/tests/static/session_end_delete_host_set_test.go +++ b/testing/internal/e2e/tests/static/session_end_delete_host_set_test.go @@ -97,9 +97,17 @@ func TestCliSessionEndWhenHostSetIsDeleted(t *testing.T) { // Check if session has terminated t.Log("Waiting for session to be canceling/terminated...") + select { + case output := <-errChan: + // `boundary connect` returns a 255 when cancelled + require.Equal(t, output.ExitCode, 255, string(output.Stdout), string(output.Stderr)) + case <-time.After(time.Second * 5): + t.Fatal("Timed out waiting for session command to exit") + } + err = backoff.RetryNotify( func() error { - // Check if session is active + // Check that session has been canceled or terminated output = e2e.RunCommand(ctx, "boundary", e2e.WithArgs("sessions", "read", "-id", session.Id, "-format", "json"), ) diff --git a/testing/internal/e2e/tests/static/session_end_delete_host_test.go b/testing/internal/e2e/tests/static/session_end_delete_host_test.go index 472d339fe7..887b7228ec 100644 --- a/testing/internal/e2e/tests/static/session_end_delete_host_test.go +++ b/testing/internal/e2e/tests/static/session_end_delete_host_test.go @@ -97,9 +97,17 @@ func TestCliSessionEndWhenHostIsDeleted(t *testing.T) { // Check if session has terminated t.Log("Waiting for session to be canceling/terminated...") + select { + case output := <-errChan: + // `boundary connect` returns a 255 when cancelled + require.Equal(t, output.ExitCode, 255, string(output.Stdout), string(output.Stderr)) + case <-time.After(time.Second * 5): + t.Fatal("Timed out waiting for session command to exit") + } + err = backoff.RetryNotify( func() error { - // Check if session is active + // Check that session has been canceled or terminated output = e2e.RunCommand(ctx, "boundary", e2e.WithArgs("sessions", "read", "-id", session.Id, "-format", "json"), ) diff --git a/testing/internal/e2e/tests/static/session_end_delete_target_test.go b/testing/internal/e2e/tests/static/session_end_delete_target_test.go new file mode 100644 index 0000000000..32743caeff --- /dev/null +++ b/testing/internal/e2e/tests/static/session_end_delete_target_test.go @@ -0,0 +1,140 @@ +package static_test + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "testing" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/hashicorp/boundary/api/sessions" + "github.com/hashicorp/boundary/testing/internal/e2e" + "github.com/hashicorp/boundary/testing/internal/e2e/boundary" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestCliSessionEndWhenTargetIsDeleted tests that an active session is canceled when the respective +// target for the session is deleted. +func TestCliSessionEndWhenTargetIsDeleted(t *testing.T) { + e2e.MaybeSkipTest(t) + c, err := loadConfig() + require.NoError(t, err) + + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + newOrgId := boundary.CreateNewOrgCli(t, ctx) + t.Cleanup(func() { + ctx := context.Background() + boundary.AuthenticateAdminCli(t, ctx) + output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("scopes", "delete", "-id", newOrgId)) + require.NoError(t, output.Err, string(output.Stderr)) + }) + newProjectId := boundary.CreateNewProjectCli(t, ctx, newOrgId) + newHostCatalogId := boundary.CreateNewHostCatalogCli(t, ctx, newProjectId) + newHostSetId := boundary.CreateNewHostSetCli(t, ctx, newHostCatalogId) + newHostId := boundary.CreateNewHostCli(t, ctx, newHostCatalogId, c.TargetIp) + boundary.AddHostToHostSetCli(t, ctx, newHostSetId, newHostId) + newTargetId := boundary.CreateNewTargetCli(t, ctx, newProjectId, c.TargetPort) + boundary.AddHostSourceToTargetCli(t, ctx, newTargetId, newHostSetId) + acctName := "e2e-account" + newAccountId, acctPassword := boundary.CreateNewAccountCli(t, ctx, acctName) + t.Cleanup(func() { + boundary.AuthenticateAdminCli(t, context.Background()) + output := e2e.RunCommand(ctx, "boundary", + e2e.WithArgs("accounts", "delete", "-id", newAccountId), + ) + require.NoError(t, output.Err, string(output.Stderr)) + }) + newUserId := boundary.CreateNewUserCli(t, ctx, "global") + t.Cleanup(func() { + boundary.AuthenticateAdminCli(t, context.Background()) + output := e2e.RunCommand(ctx, "boundary", + e2e.WithArgs("users", "delete", "-id", newUserId), + ) + require.NoError(t, output.Err, string(output.Stderr)) + }) + boundary.SetAccountToUserCli(t, ctx, newUserId, newAccountId) + newRoleId := boundary.CreateNewRoleCli(t, ctx, newProjectId) + boundary.AddGrantToRoleCli(t, ctx, newRoleId, "id=*;type=target;actions=authorize-session") + boundary.AddPrincipalToRoleCli(t, ctx, newRoleId, newUserId) + + // Connect to target to create a session + ctxCancel, cancel := context.WithCancel(context.Background()) + errChan := make(chan *e2e.CommandResult) + go func() { + token := boundary.GetAuthenticationTokenCli(t, ctx, acctName, acctPassword) + t.Log("Starting session as user...") + errChan <- e2e.RunCommand(ctxCancel, "boundary", + e2e.WithArgs( + "connect", + "-token", "env://E2E_AUTH_TOKEN", + "-target-id", newTargetId, + "-exec", "/usr/bin/ssh", "--", + "-l", c.TargetSshUser, + "-i", c.TargetSshKeyPath, + "-o", "UserKnownHostsFile=/dev/null", + "-o", "StrictHostKeyChecking=no", + "-o", "IdentitiesOnly=yes", // forces the use of the provided key + "-p", "{{boundary.port}}", // this is provided by boundary + "{{boundary.ip}}", + "hostname -i; sleep 60", + ), + e2e.WithEnv("E2E_AUTH_TOKEN", token), + ) + }() + t.Cleanup(cancel) + session := boundary.WaitForSessionToBeActiveCli(t, ctx, newProjectId) + assert.Equal(t, newTargetId, session.TargetId) + assert.Equal(t, newHostId, session.HostId) + + // Delete Target + t.Log("Deleting target...") + output := e2e.RunCommand(ctx, "boundary", e2e.WithArgs("targets", "delete", "-id", newTargetId)) + require.NoError(t, output.Err, string(output.Stderr)) + + // Check if session has terminated + t.Log("Waiting for session to be canceling/terminated...") + select { + case output := <-errChan: + // `boundary connect` returns a 255 when cancelled + require.Equal(t, output.ExitCode, 255, string(output.Stdout), string(output.Stderr)) + case <-time.After(time.Second * 5): + t.Fatal("Timed out waiting for session command to exit") + } + + err = backoff.RetryNotify( + func() error { + // Check that session has been canceled or terminated + output = e2e.RunCommand(ctx, "boundary", + e2e.WithArgs("sessions", "read", "-id", session.Id, "-format", "json"), + ) + if output.Err != nil { + return backoff.Permanent(errors.New(string(output.Stderr))) + } + + var sessionReadResult sessions.SessionReadResult + err = json.Unmarshal(output.Stdout, &sessionReadResult) + if err != nil { + return backoff.Permanent(err) + } + + if sessionReadResult.Item.Status != "canceling" && sessionReadResult.Item.Status != "terminated" { + return errors.New(fmt.Sprintf("Session has unexpected status. Expected: %s, Actual: %s", + "`canceling` or `terminated`", + sessionReadResult.Item.Status, + )) + } + + return nil + }, + backoff.WithMaxRetries(backoff.NewConstantBackOff(3*time.Second), 5), + func(err error, td time.Duration) { + t.Logf("%s. Retrying...", err.Error()) + }, + ) + require.NoError(t, err) + t.Log("Session successfully ended after target was deleted") +} diff --git a/testing/internal/e2e/tests/static/session_end_delete_user_test.go b/testing/internal/e2e/tests/static/session_end_delete_user_test.go index faf1080db7..007c7ec00b 100644 --- a/testing/internal/e2e/tests/static/session_end_delete_user_test.go +++ b/testing/internal/e2e/tests/static/session_end_delete_user_test.go @@ -90,9 +90,17 @@ func TestCliSessionEndWhenUserIsDeleted(t *testing.T) { // Check if session has terminated t.Log("Waiting for session to be canceling/terminated...") + select { + case output := <-errChan: + // `boundary connect` returns a 255 when cancelled + require.Equal(t, output.ExitCode, 255, string(output.Stdout), string(output.Stderr)) + case <-time.After(time.Second * 5): + t.Fatal("Timed out waiting for session command to exit") + } + err = backoff.RetryNotify( func() error { - // Check if session is active + // Check that session has been canceled or terminated output = e2e.RunCommand(ctx, "boundary", e2e.WithArgs("sessions", "read", "-id", session.Id, "-format", "json"), )